Skip to Content
Agents🧩 Workflow Nodes

Workflow Nodes

DocsGPT workflows are composed of Nodes that are connected to form a processing graph. These nodes interact with a Shared State—a global dictionary of variables that persists throughout the execution of the workflow.

The recording starts in a new workflow agent’s builder, adds an AI Agent node, an If / Else node and two End nodes from the palette, and moves them into place on the canvas. It wires Start → AI Agent → If / Else, and the If / Else node’s If and Else branches to one End node each. It gives the AI Agent node the “Policies & Contracts Library” knowledge source, then sets the If / Else node in Simple mode to a branch named “Credit claim” where query contains credit. It clicks Save Draft, which saves the unfinished agent and adds the Overview and Logs tabs, then Publish. It opens Preview and asks what Meridian owes a customer for a late Assured Plus delivery: the run steps through Start, the AI Agent, If / Else and End, and the answer comes from the library’s SLA schedule. The model’s wait is sped up.

NodePalette groupWhat it does
StartAdded automaticallyEntry point of the run
AI AgentCore NodesCalls a model, with optional knowledge, tools and documents
EndCore NodesFinishes the run
NoteCore NodesA comment on the canvas
Set StateLogic & DataWrites variables with CEL expressions
If / ElseLogic & DataBranches on conditions
CodeLogic & DataRuns Python in the sandbox
LoopLogic & DataRuns the nodes inside it once per item of a list

The Shared State

Every workflow run maintains a state object (a JSON-like dictionary).

  • Initial State: Contains the user’s input query (query) and chat history (chat_history).
  • Modifying State: Nodes read from this state and write their outputs back to it.
  • Node Outputs: Each node writes its result to node_<node_id>_output.
  • Documents: input_documents lists the files uploaded with the run. Once a node asks for them as text, documents holds them as numbered lines; see Line-numbered documents.

Two ways to reference state

Which syntax a field takes depends on the field, so check this table before typing {{ }} anywhere:

FieldNodeSyntax
Prompt TemplateAI AgentTemplate — {{variable_name}}
Output TemplateEndTemplate — {{variable_name}}
ExpressionSet StateCEL  — bare variable_name
ExpressionConditionCEL  — bare variable_name

Template fields substitute {{variable_name}} into surrounding text, so Analysis: {{analysis_result}} produces a string.

Expression fields are evaluated as Common Expression Language , where a bare name is the variable. Write query, not {{query}} — the braces are a syntax error there, and saving a workflow that contains one is rejected with the correction.


Start and End Nodes

Every workflow has exactly one Start node, added for you. It has no settings and can’t be deleted; the run begins there with query and chat_history in the state.

An End node finishes the run. Add End nodes from the palette where branches should stop (several branches can share one); a workflow needs at least one, and each needs an incoming connection. End nodes have no settings in the builder. A workflow saved or imported through the API can give an End node an output_template (template syntax, like a Prompt Template); its rendered text is sent to the user as the last part of the answer.


AI Agent Node

The AI Agent Node is the core processing unit. It uses a Large Language Model (LLM) to generate text, answer questions, or perform tasks using tools.

Settings

Model

  • Agent Type: Classic (retrieves from the node’s knowledge before answering) or Research (plans and searches in several steps).
  • Model: the model this node calls. The builder asks for one on every agent node; a node saved through the API without one runs on the agent’s model. A custom model set here is the workflow owner’s: whoever runs the workflow agent, the node calls it with the owner’s endpoint and key.

Prompt

  • System Prompt: instructions that define the node’s persona and constraints. Defaults to You are a helpful assistant.
  • Prompt Template: the text sent to the model as the user turn. It supports template variables, for example Summarize the following text: {{user_input_text}}. If left empty, it defaults to the initial user query ({{query}}).

Knowledge

  • Knowledge: sources this node searches.
  • Tools: tools the node’s model can call.

Both are the same pickers as in the agent form: grouped, selected items first, with Go to Knowledge and Upload new, or Go to Tools and Add tool, at the bottom. A tool you add from the node’s picker is selected on the node, and a remote device shows whether it is online. See Use knowledge in chats and agents and The Tools page.

A tool that requires approval fails the node, and with it the run: a workflow can’t pause for someone to approve a call. Turn approval off for tools you use in workflow nodes. Nodes also run only the instance guardrail floor, not the parent agent’s guardrails; see Guardrails scope.

Output

  • Output Variable: the name of the variable where the result will be saved. The result is always saved as node_<node_id>_output too; set this to something meaningful, like summary or translated_text.
  • Stream output to user: shows this node’s answer in the chat as it’s written, in addition to saving it to the state. On by default.

Advanced settings

  • Documents: which documents the node sees — All input docs, None, or Choose… a specific set (upstream artifact references or refs such as A1).
  • File passing: how a chosen document reaches the model. Auto sends the file natively when the model accepts that file type and extracts it to text otherwise; Native always sends the file (and fails on an unsupported type); Extract always sends extracted text, cut to its beginning and end when it’s long; Numbered lines sends the whole text with a [D#:L#] reference on every line (see Line-numbered documents).
  • Structured Output (JSON Schema): makes the model return JSON matching the schema. The parsed, validated object is saved as the node’s output instead of text. The model must support structured output.
  • Step details in the chat stream: how much of the finished step the chat stream carries; see Step details in the stream.

Line-numbered documents

With File passing set to Numbered lines (file_passing: lines), the node gets each chosen document’s whole text, one reference per line, so the model can cite its evidence and a later Code node can check it:

[D1:L12] 3.2. Delivery time — no more than 30 days [D2:L40] Item | Qty | Price [D2:L41] Bolt | 10 | 2.50 [D3:L7] (Sheet1!A5) Total | 12.5 [D4:L3] (p.2) Payment is due within 14 days
  • D# is the document’s position in the run’s input_documents, so every node in the run sees the same numbers. A document from elsewhere, such as a Code node’s output, is numbered after the inputs.
  • L# counts the document’s lines. Blank lines are dropped.
  • A table row is one line, its cells joined by |; the first row of a table is its header. A spreadsheet row also names its sheet and first cell, and a PDF line its page.
  • DOCX, XLSX, PDF, CSV and plain text are read with their own readers. Other formats go through the configured document parser, and a PDF without a text layer is OCR’d as usual.

The text is never cut. When the documents a node gets through Numbered lines and its prompts don’t fit the model’s context window, the node fails before calling the model, with an error that gives the token counts; select fewer documents for the node or pick a model with a larger window. A document that can’t be read fails the node the same way, naming the file. Each document is sent inside its own <document> tag, and the model is told to read what’s inside as data, not instructions.

Text a Prompt Template pulls in from documents isn’t measured up front: when it doesn’t fit, the node fails with the usual context-length error instead.

Each document is parsed once per run, on the parsing worker: a second node reading it gets the same text. The text is also cached by the file’s content for WORKFLOW_DOCUMENT_LINES_CACHE_TTL seconds (a day by default; 0 turns the cache off), so running the workflow again over the same file skips the parse.

The documents are also in the state as documents, a list with one entry per document read so far:

KeyValue
idD1, D2, …
artifact_id, filename, mime_typeThe document
line_countNumber of lines
tablesOne {first_line, last_line, header} per table
textThe numbered lines

A Prompt Template or Output Template that reads documents (for example {% for d in documents %}{{ d.text }}{% endfor %}) loads every input document first, and so does a Code node that lists documents in its Input files. A Code node that doesn’t list it gets no documents in its state.json, even when an earlier node read them. The run record and the step events keep the list without text and without the table headers. A workflow that writes its own documents variable keeps it; the engine then leaves that variable alone and reads no documents for it.


Set State Node

The Set State Node allows you to manipulate variables within the shared state directly without calling an LLM. This is useful for initialization, formatting, or control flow logic.

Operations

You can define multiple assignments in a single node. Each assignment has two parts:

  • Variable: the state key to write to.
  • Value (CEL): a CEL  expression evaluated against the current state. Its result becomes the variable’s value.

Reference state variables by bare name. {{ }} is template syntax and does not work here — an operation must supply both parts, or it is rejected when you save.

GoalVariableValue (CEL)
Initialize a counterretry_count0
Increment a counterretry_countretry_count + 1
Copy a node’s output to a stable namecontextnode_search_1_output
Build a stringformatted_response"Analysis: " + analysis_result
Append to a listhistory_listhistory_list + [last_result]
Derive a booleanneeds_reviewsize(context) < 100

CEL supports arithmetic, string concatenation with +, comparisons, &&/||, ternaries (cond ? a : b), and built-ins such as size(), startsWith(), and contains().

Usage Examples

  • Loop Counters: Initialize retry_count to 0 before a loop, then set it to retry_count + 1 inside the loop.
  • Accumulators: Collect results across branches with history_list + [last_result].
  • Renaming: Copy a previous node’s output to a generic name (target context, expression node_search_1_output) so later nodes can use one standard variable.

Condition Node

The Condition Node (If / Else in the palette) branches the workflow. Each case pairs a condition with an outgoing branch; the first case whose condition is true wins, and execution follows that branch.

The node has two modes:

  • Simple: pick a Variable, an Operator (=, !=, >, <, >=, <=, contains or starts) and a Value. The builder turns each row into a CEL expression, for example category == "refund" or query.contains("invoice"). The variable can be a dotted path such as audit.high_risk_accounts, and every case must name one.
  • Advanced: write each case as a CEL  expression yourself.

Both modes run the same way:

  • Expressions use the same syntax as the Set State node: reference state by bare name, not {{ }}.
  • Every condition node needs an else branch, which is taken when no case matches.
  • Each case with an expression must have an outgoing edge, and every branch must eventually reach an end node, except inside a loop.
GoalExpression
Route on a previous answernode_classify_1_output == "refund"
Guard on retrieved contextsize(context) > 0
Combine checksneeds_review && retry_count < 3
Match textquery.contains("invoice")

A case whose expression fails to evaluate at run time is skipped and the next case is tried, so a workflow that always lands on else usually means an expression is referencing a variable that no earlier node writes.


Code Node

The Code Node runs a Python script in a sandboxed session bound to the workflow run. Use it to transform data, parse files, cross-check documents, or build a report that later nodes consume. Every Code node and tool call in one run shares the same session.

  • Code: the script to run. It is never templated: {{ }} in the code is literal text. The script reads the workflow state from state.json in its working directory (chat_history is left out):

    import json state = json.load(open("state.json")) total = len(state["query"])

    The script can’t change the shared state. Editing state.json has no effect on later nodes; pass results through the node’s output instead.

  • Input files: artifacts to stage in the workspace before the script runs, under inputs/<filename>. Each entry is a state variable holding an artifact reference, a full artifact id or a short ref such as A1 from this run. The entry documents stages no file: it puts every input document’s numbered text in state["documents"] (see Line-numbered documents), so the script can check a model’s [D#:L#] citations without parsing the files itself.

  • Output Variable: an extra name for the node’s output, which is always saved as node_<node_id>_output.

  • Timeout (seconds): optional. It can’t exceed SANDBOX_EXEC_TIMEOUT, which is also the default.

  • Step details in the chat stream: see Step details in the stream.

  • Structured Output (JSON Schema): optional; see the output rules below.

What the node outputs:

SetupNode output
No schema, the script created or changed filesA reference to the first captured artifact (artifact_id, version, mime_type, filename, …)
No schema, no files{"artifacts": [], "status": "ok"}
JSON schema setThe JSON the script prints to stdout, validated against the schema. When files were captured too, they are added under an artifacts key.

Use a schema when later nodes need to branch on a decision: print one JSON object, for example print(json.dumps({"approved": total > 10})), then test node_code_1_output.approved in an If / Else node. Stdout that isn’t valid JSON, or doesn’t match the schema, fails the node.

Files the script creates or changes are saved as artifacts and shown in the run view; files under tmp/ and temporary files (.tmp, .lock, .pyc) are skipped. A script that raises an error fails the node and the run. See Artifacts and Code Execution for the sandbox backends and configuration.


Loop Node

The Loop Node takes a list from the state and runs the nodes inside it once for each item. It is drawn as a container: drag nodes from the palette onto it (or select the loop and click a palette pill) and connect them to each other. The loop itself connects to the rest of the graph like any other node; the nodes inside it connect only to each other.

  • Items: the state path of the list, for example extracted.items. Dotted segments read object keys and list positions (rows.0). A string holding a JSON array, such as an AI Agent node’s answer without a schema, is read as that array. Anything else fails the node.
  • Item variable: the name the current item has inside the loop, item by default. Prompt templates read {{ item.name }}; Set State and If / Else expressions read item.name. It can’t be a reserved name such as query, chat_history, input_documents or a template namespace.
  • Items at once: how many items run side by side, 4 by default. A higher value is lowered to WORKFLOW_LOOP_MAX_CONCURRENCY (4 by default).
  • Most items: a longer list fails the node before any item runs. The node default is 50, and a higher value is lowered to WORKFLOW_LOOP_MAX_ITEMS (100 by default).
  • When an item fails: Stop and fail the loop (the default) fails the node and writes nothing; items not yet started don’t run and running ones stop at their next node. Leave the item out drops it from the results. Keep the error in its place puts {"error": "..."} in that item’s slot. An item whose body is still going after 50 node steps counts as failed too.
  • Output Variable: an extra name for the results, which are always saved as node_<node_id>_output. The results are a list in the same order as the items.
  • Item result: optional. A variable the nodes inside set; its value becomes the item’s result. Without it the result is the output of the last AI Agent or Code node that ran for the item.

Each item runs on its own copy of the state: it sees everything set before the loop, and nothing it sets (including the item variable and the inner nodes’ outputs) reaches the state after the loop or the other items. Only the results list is written back.

The nodes inside a loop can be AI Agent, Code, Set State, If / Else and Note nodes. A loop can’t hold Start, End or another loop. The first node of the body is the one no other inner node connects to, and there must be exactly one. An item ends where its path has no next node, so an If / Else inside a loop doesn’t need its branches to reach an End node.

Inside a loop, AI Agent nodes don’t stream their text to the chat, since items run at the same time. Code nodes in a loop take turns on the run’s sandbox session. The 50-step limit applies to each item separately, and the loop counts as one step of the run.

During a run, the chat and the Preview show how many items have finished. Each inner step’s workflow_step stream event carries parent_node_id and item_index, and a workflow_loop_item event marks each item starting and ending. The run record keeps one step for the loop with each item’s steps nested under items.

In the stored graph, a node inside a loop names the loop in its data as parent_id:

{"id": "each_item", "type": "loop", "data": {"items": "extracted.items", "item_variable": "item", "concurrency": 4, "max_items": 50, "on_item_error": "fail", "output_variable": "results"}} {"id": "summarize", "type": "agent", "data": {"parent_id": "each_item", "prompt_template": "Summarize {{ item.name }}", "json_schema": {"type": "object"}}}

Retrying a failed node

A temporary upstream failure, such as a 503 from the model provider or a dropped connection, fails a node and with it the run. AI Agent and Code nodes can retry such failures with a pause between attempts before they give up. Retries are off by default.

Set them in the workflow’s details, under Retries, where every AI Agent and Code node inherits them, or on one node: under Advanced settings for an AI Agent node, at the end of the panel for a Code node. A node’s own settings win field by field. Default keeps the inherited value, and 1 (no retries) turns retries off for that node.

  • Max attempts: how many times the node runs at most, from 1 to WORKFLOW_RETRY_MAX_ATTEMPTS (5 by default). 1 means no retries.
  • Retry delays (s): the pause before each retry, as numbers separated by commas. With 2, 10, 30, the second attempt waits 2 seconds, the third 10, and any later one 30. Defaults to 2, 10, 30. A 429 or 503 that sends Retry-After waits that long instead.
  • Retry on: which failures count, for AI Agent nodes. Both are on by default, and at least one stays on.
    • Errors and timeouts: a 5xx or 429 response, a connect or read timeout, a reset connection, or an empty reply from the model.
    • Invalid output: the reply isn’t valid JSON or doesn’t match the node’s JSON schema. The next attempt’s prompt includes the validation error.

A Code node retries only when the sandbox itself failed: it was unreachable, its runtime was gone, or it dropped the connection mid-run and the running script could be stopped. An error raised by the script, a timeout, a kernel that died running the script (out of memory or a crash), a refused sandbox login, or stdout that doesn’t match the schema is never retried.

Some failures are never retried, whatever the settings: a 400 or other client error, an authentication error, a request too long for the model’s context window, an exhausted usage quota, and a run out of time. A node also fails at once when:

  • it already streamed text to the chat, which can’t be taken back. A node with a JSON schema and Invalid output retries holds its reply until it validates, so that it can be retried;
  • the pause would end past the run’s time limit, since pauses count toward it;
  • the server’s Retry-After is longer than WORKFLOW_RETRY_MAX_DELAY_SECONDS (300 by default).

Each attempt runs the whole node again, tool calls included, so a tool with side effects may run more than once. An empty reply on the last attempt is kept as the node’s output, as it is without retries.

While a node waits to retry, the chat and the Preview show the attempt it is on, and a finished step shows how many attempts it took. Retries inside a loop count toward the loop’s items instead. The retry is a workflow_step event with status: "running" and attempt, max_attempts, retry_delay_seconds, retry_reason (transient or invalid_output) and retry_error; the step’s final event carries attempts. The run record lists each attempt of a retried node under the step’s attempts, with its status, timing, error and reason.

In the stored graph, a node keeps its settings under retry in its data, and the workflow keeps the inherited ones as retry_defaults (also accepted by POST /api/workflows and PUT /api/workflows/<id>):

{"id": "extract", "type": "agent", "data": {"json_schema": {"type": "object"}, "retry": {"max_attempts": 3, "backoff_seconds": [2, 10, 30], "on": ["transient", "invalid_output"]}}}

Note Node

A Note holds free text for whoever edits the workflow: what a branch is for, or what still needs doing. It has no effect on a run.


Graph rules

The builder checks these before a preview or publish, and before a save once the workflow is published, and lists every rule a workflow breaks. A draft saves without them: Save draft, beside Publish until the workflow is published, keeps an unfinished workflow and needs only a name. A chat with a draft whose workflow isn’t finished, by you or a teammate it is shared with, answers with an error instead of running it. The workflow’s graph and its name, description and image are saved together, so a save either stores all of them or none.

  • The workflow has a name, exactly one Start node, at least one End node and at least one AI Agent node.
  • The Start node connects to another node, and every End node has an incoming connection.
  • Every AI Agent node has a model selected. A node with a JSON schema needs a valid schema and a model that supports structured output.
  • Every If / Else node has at least one case with an expression, an Else branch and at least two outgoing connections. Each case with an expression has exactly one connection, each connection comes from a known case, and in Simple mode each case names a variable.
  • Every If / Else branch eventually reaches an End node, except inside a loop, where a branch ends the item.
  • Every Code node has code, and any JSON schema it has is valid.
  • Retry settings, on a node or the workflow, stay within the limits in Retrying a failed node.
  • Every Loop node names a list as a state path, has a usable item variable, and holds at least one node with exactly one first node. No connection crosses a loop’s edge.

A run that is still going after 50 node steps stops with an error and is recorded as failed, so a cycle in the graph needs an exit condition.

Step details in the stream

While a workflow runs, the chat stream sends a workflow_step event as each node starts and finishes. A finished step’s event carries the node’s output and the state it changed, so the chat and the builder’s Execution details can show them. A node that reads documents can produce a lot of both, so the event is held to WORKFLOW_STREAM_MAX_EVENT_BYTES (32 KB by default):

  • When the output and state changes fit, the event carries them. The output is sent once: the node_<node_id>_output copy is left out of state_delta.
  • When they don’t, the event is a summary: summarized: true, the output itself when it is small or else output_preview (its first 500 characters) with output_truncated: true, output_bytes, and state_keys_changed listing the keys the node set. A very long key list is cut to fit and marked state_keys_truncated.

Every finished step’s event also carries step_index, its place in the run’s saved steps (a loop item’s steps, saved inside the loop’s step, carry none). The run keeps every step’s full output and state changes (except the text and table headers of documents, which runs never store), so Show full output on a summarized step in Execution details loads them once the run ends. A preview of a workflow that was never saved stores no run: save the workflow and run it again to load a large step’s full output. Over the API that is GET /api/workflow_runs/<run_id>/steps/<step_index> (a personal access token needs runs:read).

Each node can change this with Step details in the chat stream (stream_output in an exported or API-saved workflow; any node type accepts it):

SettingThe finished step’s event carries
Summary when large (summary, default)The output and state changes, or a summary when they exceed the limit
Always full (full)The output and state changes, whatever their size
Status only (none)The status alone, with output_omitted: true

This setting covers the finished step’s event only. An AI Agent node’s answer streams into the chat while Stream output to user is on, whatever this setting says. To keep a node’s output out of the chat stream, set Status only and turn Stream output to user off, and make sure no later node shows it: an End node’s output template, or another node that streams to the user, can still put the node’s values into the answer. Both settings limit what the stream carries, not who can read the output: the run still saves it, and the person who started a saved run can read every step through the step route above. Use Always full only when a client needs every value from the stream itself. An End node’s output template is streamed in pieces under the same limit.

One workflow per agent

A workflow belongs to the agent it was built in, and deleting a workflow agent deletes its workflow. Over the API, creating an agent (POST /api/create_agent) with a workflow that another agent already uses, or switching an agent to one with PUT /api/update_agent/<agent_id>, answers 409 with “This workflow belongs to another agent”. Create a new workflow for the new agent instead.

The builder saves through those same two endpoints: it sends the graph as a workflow_graph field ({"nodes": [...], "edges": [...], "retry_defaults": ...}, a JSON string in a multipart form) with the agent’s other fields, in one request. With "status": "draft" only the graph’s shape is checked; with "published" the full graph rules apply. They also apply to a draft that something can still run: an agent published before (its API key keeps working), one with a webhook URL, a public link or an active or paused schedule, and one whose workflow another such agent also runs. A draft holding an unfinished workflow gets no webhook URL, schedule or public link (400 with the broken rules) until its graph passes them. A request can send workflow_graph or workflow, not both. POST /api/workflows and PUT /api/workflows/<id> keep working for scripts that manage the graph on its own.

Last updated on