Прескокни до содржината

Workflow steps

Оваа содржина сè уште не е достапна на вашиот јазик.

A workflow can contain steps as well as agents. A step is one fixed piece of work, such as classifying a message, searching a collection or writing a reply, that runs the same way every time. Steps let you script the parts of a conversation that should not be left to an agent’s judgement, and keep agents for the parts that should.

This page is the reference. For what a workflow is and how agents hand over to each other, see Workflows. A workflow with no steps works exactly as described there.

This workflow answers policy questions from a collection and sends everything else to a triage agent. A router step reads the message and chooses a branch. On the policy branch a retrieve step searches the handbook collection and an llm step writes the cited answer. On the other branch the conversation goes to triage, which can transfer to specialist.

description: Answer policy questions from the handbook; route the rest to triage
agents:
- triage
- specialist
steps:
classify:
kind: router
title: Classify the question
branches:
- name: policy
description: The user asks what the company policy says
- name: other
description: Anything else
search:
kind: retrieve
collections: [handbook]
query: "{{ input.message }}"
top_n: 5
retry: {max: 1, backoff_ms: 500}
answer:
kind: llm
title: Policy answer
reply: true
system: Answer only from the context. Cite sources as [ref].
prompt: |
Question: {{ input.message }}
Context:
{{ search.context }}
edges:
- {from: start, to: classify}
- {from: classify, to: search, branch: policy}
- {from: classify, to: triage, branch: other}
- {from: search, to: answer}
- {from: triage, to: specialist, kind: transfer, when: The user needs a specialist}

Agents are written by name here, as the console shows them. start is the box every run begins at; an edge from it replaces entry. The console’s Scripted RAG chat template creates a workflow of this shape for you: you choose one collection you can read, and it creates the starter agents, named after the workflow by default (<workflow>-triage and <workflow>-specialist; you can rename them in the dialog), along with the steps and edges.

On top of the keys in Workflows, a workflow with steps may use:

  • steps — the steps, by name. At most 100.
  • vars — variables that live for the whole conversation, see Variables. At most 50.
  • guardrails — the same settings an agent has, applied to the whole workflow, see Guardrails.
  • route_every_turn — true makes every message start at the step the Start box leads to, even while an agent holds the conversation. See Routing every message.
  • limits.max_steps — a ceiling on step and agent runs in one run.

Step and variable names start with a lowercase letter and then use lowercase letters, digits and underscores, up to 64 characters in all. start and end are reserved. Steps and agents share one namespace, so a step may not have the same name as an agent. A step may carry a title of up to 80 characters.

An entry in agents is either a name, or {agent: name, input: "..."} for a pipeline agent that is given a task.

Every step has a kind. Three settings are common to all of them:

  • title — a label for people. Replies written by a step carry the step’s name in the chat; a title is shown as the label when the step has one. Without a title the label is the step’s name; the chat shows the title the step had when that run happened.
  • retry — {max, backoff_ms}, see Errors and retries.
  • on_error — fail, continue or branch, see the same section.

The step kinds below are the ones you can use today, with eight more for unattended runs and the approval and ask steps.

Calls a chat model with a prompt.

field
prompt Required. The message sent to the model. A template.
system System instructions. A template.
model A chat model from the model registry, by alias. Left out, the workspace’s utility model is used.
output_fields A list of fields the model must return, see below.
reply true shows the result in the chat. Default false.
history How many earlier conversation messages to send before the current one, 0 to 20. Default 0. Agent messages are sent as text only.

With reply: true the text streams into the chat as the model writes it, under the step’s name. Without it the result is kept for later steps and the user does not see it.

output_fields asks the model for structured output. Each field has a name, a type (string, number, integer, boolean or array) and optionally description, required, options (the allowed values, on a string only) and items (the element type, on an array only). There are at most 30 fields. A reply that does not fit the schema is a step error. You cannot combine output_fields with reply: true.

Output: text, or one entry per declared field.

Chooses a branch by testing values, with no model call.

branches is an ordered list of {name, when}, 1 to 20 of them, with unique names. The first whose rule is true wins; if none is, the run takes the else branch. See Conditions for the rule syntax.

Output: branch, the name of the branch taken.

Asks a model to choose a branch. It is the step to use when the answer depends on what the user meant, not on a value you can test.

field
branches Required. 2 to 10 of {name, description}. The descriptions are what the model decides by.
input What to classify. A template; default {{ input.message }}.
model A chat model or a decision model, by alias. Left out, the workspace’s decision model answers when one is set, otherwise the workspace’s utility model.
min_confidence 0 to 1. When a decision model answers less sure than this, a chat model decides instead. Default 0.3.

A decision model answers in a fraction of a second and gives a confidence with its choice. It writes no reason. The model’s choice is never shown in the chat.

Output: branch, reason (empty when a decision model answered), confidence and probabilities (empty when a chat model answered), engine (decision or llm) and fallback_reason (why a chat model decided instead of the decision model, or empty).

Asks a model questions about the input: pick one of a set, yes or no, or a score on a scale. Each answer comes with how sure the model was, so a later condition can branch on the answer or on its confidence. Classify has no branches of its own and never writes to the chat.

field
questions Required. 1 to 10 questions, see below.
input What to classify. A template; default {{ input.message }}.
model A chat model or a decision model, by alias, as for router.
min_confidence 0 to 1, as for router. Only the questions answered less sure than this go to a chat model. Default 0.3.

Each question has:

field
name Required. Lower-case letters, digits and _; not engine or fallback_reason.
type Required. choice, yes_no or score.
instructions Required. The question, in plain text.
options For choice: 2 to 20 of {name, description}.
levels For score: 2 to 10 of {name, description}, lowest first.
steps:
triage:
kind: classify
questions:
- name: urgency
type: choice
instructions: How urgent is this request?
options:
- {name: high, description: Service down or a deadline today}
- {name: low, description: Anything that can wait}
- name: is_complaint
type: yes_no
instructions: Is the customer complaining?
escalate:
kind: condition
branches:
- name: urgent
when:
all:
- {left: "{{ steps.triage.urgency.answer }}", op: equals, right: high}
- {left: "{{ steps.triage.urgency.confidence }}", op: gte, right: 0.8}

Output: one entry per question, holding answer, confidence and probabilities, and position for a score (a number from 0 for the lowest level up to one less than the number of levels). A yes-or-no answer is true or false. When a chat model answered a question, its confidence, probabilities and position are empty. Also engine (decision, llm or mixed) and fallback_reason.

Searches one or more collections.

field
collections Required. 1 to 5 collection names, written out in the step.
query A template; default {{ input.message }}.
top_n How many results to return, 1 to 50. Left out, the collection’s own result count is used (8 unless the collection says otherwise).
filters Metadata filters, as for the agent search tool. See Metadata & filters.

Collection names are always literal. A template cannot choose what is searched, so nothing a user types or a model writes can move a search to a different collection. The search runs as the person using the workflow, so a collection they cannot read returns nothing. When you save, you must be able to read every collection you name.

Output: results (the rows search returns), context (the same rows as text, each as [ref] content, ready for a prompt) and count. Citations from retrieve steps are attached to the replies that cite them, as they are for an agent, see Citations.

Writes variables.

values maps a variable name to a template or a literal. Only declared variables can be written. A literal is checked against the variable’s type when you save; a rendered value is checked when the step runs.

Output: the values written, by variable name.

Adds a fixed message to the chat.

text is required, and is a template. It appears whole, not streamed.

Output: text.

Pauses the run until a person decides, then continues on one of three branches: approved, rejected or timed_out.

approve:
kind: approval
approvers: {roles: [admin], members: [ana@example.com]}
message: "Send this reply to {{ input.body.requester }}?"
edit: "{{ steps['ui/desk-drafter'].text }}"
expires_in: 3d
  • message is required, and is a template shown to the approver.
  • approvers is optional. roles lists workspace roles (admin, editor, viewer) and members lists email addresses, at most 50. Either or both. They are written out literally, never as templates, so a webhook payload cannot choose who approves. Each address must belong to a verified workspace member.
  • edit is an optional template. The approver sees it in a text box and may change it. Later steps read the result as {{ approve.text }}, and {{ approve.edited }} says whether it was changed.
  • expires_in is how long the request waits, from 1h to 30d, and defaults to 3d. It applies only with approvers.
  • allow_self lets the person who started the run approve it. It defaults to false, and applies only with approvers.

All three branches must be connected. Output: branch, approver (the decider’s email, empty after a timeout), reason, decided_at, and, with edit, text and edited. An approval step has no retry or on_error, and it cannot be inside a for_each or loop.

Without approvers, the person in the chat decides, exactly as for an approval on an edge. There is no request, no mail and no expiry. Such a step cannot be in a workflow that has a trigger, because nobody would see the request.

With approvers, the approvers are mailed a link to the Approvals page. A decision is accepted from a current member whose role is named, or whose verified email is named. The person who started the run cannot decide it, unless allow_self is on; for a triggered run that person is the trigger’s owner. An expired request takes timed_out, and a decision on an expired request is refused. A run continues only on a decision the platform recorded.

Named approvers are not available in a chat started from an embed or in a run started with an API key: start such a workflow from a trigger or the console. A workflow with named approvers cannot have an embed, and the check runs both when the step is saved and when the embed is created.

A chat that begins a new turn which reaches the same step asks again; each turn makes its own request.

The Approvals page lists requests that name you, with a count in the navigation. Waiting shows requests you can decide now, oldest first. Decided shows the last 30 days. Open a request to read the message, change the text when the step offers one, add a reason, and approve or reject. If someone decided first, the page tells you who and when.

Workspace admins see every request, read-only, and decide only those that name them. The person who started the run sees theirs with no buttons.

Pauses the run on a question for the person in the chat, then continues with the answer.

level:
kind: ask
title: Choose a level
question: "I found material on {{ topic.answer }}. Which level?"
ask: choice
options: [1 – easy, 2 – medium, 3 – challenging]
  • question is required, and is a template.
  • preface is optional, a template of up to 2,000 characters shown above the question: feedback on the previous answer, a score. Empty means none.
  • ask is confirm, choice, multi, scale or text.
  • options (choice and multi) is a list of 2–10 labels, each a template, or one template that renders to a list.
  • progress is {current, total}; scale is {min, max, min_label, max_label} (default 1–5).

The person can tap an answer or type one; a typed answer goes through the workflow’s input guardrails. An answer that does not fit (an option that does not exist, a number out of range) asks again with the reason.

Outputs: answer (the answer as text, always set), selected (the chosen labels), value (the number) and confirmed (true or false); the ones that do not apply are null. Branch on them with a condition step.

An ask step cannot run inside a for_each or loop, takes no retry or on_error, and cannot be in a workflow that has a trigger.

One ask step can ask a whole list, such as a practice round, and grade it.

quiz:
kind: ask
title: Practice
ask: choice
items: "{{ draw.questions_json | fromjson }}"
preface: "Let us start."
  • items replaces question, options, progress and scale, and works with ask: choice or multi. It is a template that renders to a list of 1–20 questions, either a list value or text holding a JSON list. A malformed list fails the step and names the item at fault.
  • Each item is {question, options, correct, explanation, id}. question and options (2–10 labels) are required. correct is optional: the correct option’s label, its number counted from 1, or a list of either for multi. explanation (up to 1,000 characters) is shown after the answer. id is passed through to the outputs.
  • The questions are asked one at a time, each showing its place (2 of 5). preface is shown above the first question only. Above each later question the person sees feedback on the previous answer, ✓ Correct! or the correct answer, followed by the explanation, when that item has a correct. A typed answer is graded by matching its text, ignoring case, to an option label or number.
  • When the last answer is in, the chat shows a review: the score, and for each question the answer given and, when it was wrong, the correct one.

Outputs: answers (a list of {id, question, selected, answer, correct}, correct being true, false or null when the item has no correct), score (the number correct, null when no item has a correct) and total. The single-question outputs are null. A later step can use them, for example {{ quiz.score }} of {{ quiz.total }}.

Eight more kinds are for workflows that run on a schedule or from a webhook. They are described in Triggers and automation.

  • for_each runs a nested flow once for each item of a list. An optional progress_label names each item while the run is watched.
  • loop repeats a nested flow until a condition holds, up to a limit.
  • http calls a web address and reads JSON, text or an RSS/Atom feed.
  • email sends a plain-text email to workspace members or verified domains.
  • ingest adds documents to a collection, from web addresses or from text, through the collection’s import queue.
  • documents lists, reads and sets metadata on a collection’s documents. A read returns up to 100,000 characters, which is what still fits in the agent input a template builds; limit and max_chars may come from a template.
  • code runs a short piece of Python in an isolated sandbox.
  • tool calls a tool from a registered integration.

An agent in agents can be a step in the flow too. An agent that has no outgoing then edge and is reachable from start is a conversation holder: it answers the user and keeps the conversation, see the turn rule. An agent with a then edge is a pipeline agent. It runs in the background, gets the conversation (or the task you give it in input, a template rendered as one user message), and its final text goes to steps["name"].text for later steps. It does not talk to the user. A pipeline agent cannot have transfer edges.

Output: text.

A flow edge has a from, a to and a kind.

kind meaning
then Continue to the next step. At most one per step or agent.
branch Take this edge when a condition or router chooses the label named in branch:.
error Take this edge when the step fails and has on_error: branch.
transfer and delegate Between agents, as described in Workflows.

On an edge that touches a step you can leave kind out: it is then, or branch when the edge has a branch: label. Between two agents it is required.

The rules the editor checks when you save:

  • Every edge from start names where a run begins. A file with one may leave out entry; if it keeps entry, it must name the same target.
  • Every label a condition declares, plus else, and every label a router declares, has exactly one branch edge, and no edge names a label that does not exist.
  • A condition or router has no then edge.
  • A step with on_error: branch has exactly one error edge; any other step has none.
  • Any edge may point to end, which finishes the run. Nothing leaves end.
  • then, branch and error edges cannot form a cycle. A flow only moves forward.
  • Every step must be reachable from start. A step with no edge out ends the run.
  • A transfer may only leave and arrive at an agent that can hold the conversation.

Wherever a field says “a template”, you can put values into it with {{ ... }}.

name what it holds
input.message The user’s latest message.
vars.<name> A variable.
run.id, run.thread_id, run.started_at, run.source Facts about this run.
<step>.<field> A field of an earlier step’s output, such as search.context.
steps["<agent>"].text An agent’s output, where the name is not a plain identifier, such as steps["hr/triage"].text.

A reference to a step is accepted only when that step comes before the current one in the flow, and only for a field that step declares. The editor checks this when you save. A step that did not run reads as empty.

When a field’s whole value is one reference, such as query: "{{ input.message }}", it keeps the value’s type: a list stays a list. In any other template the result is text, with lists and objects written as JSON.

Templates use a small, deliberately limited expression language:

  • Allowed: attribute and index reads, slices, comparisons, and, or, not, +, -, ~ (join as text), x if cond else y, short list and object literals, is tests and {% if %} blocks.
  • Not allowed: function and method calls, loops, {% set %}, macros, includes, and every arithmetic operator other than + and -. The editor names the problem when you save.
  • Filters: abs, capitalize, default, first, float, int, items, join, last, length, list, lower, map (as map(attribute="name")), max, min, replace, reverse, round, selectattr, string, title, tojson, fromjson, trim, truncate, upper and wordcount.

Example: {{ search.results | map(attribute="title") | join(", ") }}.

fromjson turns JSON text into data, so a flow can read a request sent as JSON. It takes text of at most 64 KiB, and bad JSON is a step error. With a declared variable folder:

request:
kind: set
values:
folder: "{{ (input.message | fromjson).folder }}"

Limits: a template may use at most 100 filters, and may write at most 20 items in list and object literals in total. A result larger than 256 KB is a step error.

Only the text you write is treated as a template. What a user types, or what a model returns, is a value inside it and is never run.

In the console, every template field has an Insert reference picker that lists input.message, the variables, run, and each earlier step with the fields it produces, and inserts the {{ ... }} at the cursor.

A rule is {left, op, right}. left and right are templates or plain values. Rules can be combined with {all: [...]} and {any: [...]}, nested up to three deep. A condition step holds at most 50 rules in all.

operator true when
equals, not_equals The two sides are equal, or not.
contains, not_contains left contains right.
starts_with, ends_with left starts or ends with right.
regex left matches the pattern in right.
empty, not_empty left is empty, or not. Takes no right.
gt, gte, lt, lte A numeric comparison. If either side is not a number the rule is false.
in, not_in left is in the list right. right must be a list.

A regular expression is written out in the rule, not built from a template. It can be at most 500 characters and is checked when you save. It runs against the first 10,000 characters of left and is stopped after 100 milliseconds, which is a step error. \p{...} classes and fuzzy-matching syntax are not supported, so use character ranges instead.

In the console, a condition’s branches are edited as a rule builder: rows of left, operator and right, grouped by all or any.

vars declares named values that last for the whole conversation, not just one run, so a later turn can read what an earlier one stored.

vars:
customer_tier:
type: string
default: standard

The type is string, number, boolean, list or object; default is optional. A set step writes them and any template reads them as vars.customer_tier. In the console they are edited under the workflow’s Settings.

Variables are not an access control. Do not use one to decide who may see something.

A workflow can carry its own guardrails, in the same shape as an agent’s. The input check runs once when a run starts, on the user’s message; if it refuses, the refusal is the reply and the run ends. The output check runs on the text of every reply step and every llm step with reply: true, before it is shown. A step whose result only feeds later steps is not checked; the final reply is. An agent in the flow still applies its own guardrails as well. See Trust tiers for how a workflow’s agents are limited. In the console, guardrails are edited under Settings.

A conversation continues across several messages. After an agent in the flow answers the user, it holds the conversation: the user’s next message goes straight to that agent, and the steps before it do not run again. If that agent transfers to another, the new agent holds it instead. If no agent has answered yet, a message starts at start.

In the example above, a first message that classifies as other goes to triage. From then on triage answers, so a follow-up policy question goes to triage, not back through the router. That is why triage should also be able to answer policy questions from the collection, when the conversation might wander into them.

A flow that ends without any reply tells the user so, in one fixed message, and records a warning. A step that only writes variables or classifies, and then ends, therefore never leaves the chat silent.

Any step can set retry and on_error, except that retry is refused on for_each and loop (retry the steps inside them instead).

retry is {max, backoff_ms}. max is the number of extra attempts, 0 to 5. backoff_ms is the wait before the first retry, 0 to 60,000, and the wait doubles on each later attempt. Approvals and transfers are never retried (an approval step takes neither setting), and neither are a run exceeding a limit, a missing model, or a trial workspace calling a model with no price.

When a step still fails after its retries, on_error decides:

value what happens
fail The default. The run ends with an error. For a step, the error names the step; for a limit, it names the limit.
continue The step’s output is empty and the flow follows its then edge. A condition or router that fails this way has no branch to take, so the run ends. Exceeding a limit always fails the run, whatever on_error says.
branch The output is empty and the flow follows the step’s error edge.

A failed step is recorded on the run either way. Only an error message that the platform wrote itself, such as an invalid template or a limit, is kept. Provider and driver errors keep their type but not their text, because the text can reveal internal addresses.

limit default maximum
max_steps 200 1,000
max_handoffs_per_turn 5 20
max_model_calls 50 200

max_steps counts every step and agent run in one run. A run that goes past it ends with an error that names the limit. Other bounds: at most 100 steps, 50 variables and 20 agents in a workflow, 20,000 characters in a prompt or template field, and 256 KB for one step’s output. In the console, the three limits are edited under Settings.

The workflow page has a Runs tab, which lists the workflow’s past runs, newest first, from every source: the console chat, the API, an embed. Each row shows when it started, its source, who ran it (you, or the kind of caller: another user, an API key, an embed), its status, how long it took and what it cost.

Cost is the model calls made by the workflow’s agents and steps. Search planning, embeddings, reranking and guardrail checks are not included.

Opening a run shows the workflow with each step’s status, timing and cost on its box. A box that did not run is dimmed once the run has ended. Open a step to see what it received and what it returned, and for a failed step its error.

Who sees what:

  • Anyone who can read the workflow sees the list of runs.
  • A step’s input and output show on your own runs. Editors of the workflow and tenant admins also see them on every run.
  • Anyone else sees a run’s status, timings, cost and the error type, but not its message.
  • An agent in the workflow that you cannot read appears as “An agent”, with no content. Text it produced can still appear in a later step’s input or output.

A workflow that runs for minutes shows where it is while it runs.

  • Chat. Under your last message the chat shows one line: the step that is running, such as “Find the documents…”, or for a for_each or loop its title, how far it has got and what is in flight, such as “Each document — 3 of 10 · Статини.pdf · 1 failed”. A loop reads “iteration 3 of at most 10”. Screen readers hear the line at most every ten seconds.
  • Canvas. While a run goes, every box that is working is lit and a band of light circles its border; a for_each or loop shows its count (“3/10”) and its body’s busy steps are lit too. With reduced motion turned on in your system, boxes are lit but do not animate.
  • API. A caller that does not stream can poll GET /api/manage/workflows/{workflow_id}/runs/{run_id}/progress with the same Bearer key. See Threads and runs.

Name the items with progress_label on a for_each or loop: a template that can read the same names as the container’s body (item and index in a for_each; iteration and previous in a loop), rendered for each item and cut to 80 characters.

each:
kind: for_each
title: Each document
items: "{{ find.documents }}"
progress_label: "{{ item.filename }}"

A label that fails to render leaves that item unnamed and never fails the run. At most five labels are shown at once. Labels are text from your own data, so they follow the same rule as a step’s input and output: you see them on your own runs, and editors of the workflow and workspace admins on every run. Anyone else sees the counts only.

Each run is one trace, with a span for every step and, at debug level, every pipeline agent. A step’s model calls count against max_model_calls and appear as “Steps” in the Monitor tab’s cost-by-agent table. See Read traces and evals.