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.
| Node | Palette group | What it does |
|---|---|---|
| Start | Added automatically | Entry point of the run |
| AI Agent | Core Nodes | Calls a model, with optional knowledge, tools and documents |
| End | Core Nodes | Finishes the run |
| Note | Core Nodes | A comment on the canvas |
| Set State | Logic & Data | Writes variables with CEL expressions |
| If / Else | Logic & Data | Branches on conditions |
| Code | Logic & Data | Runs Python in the sandbox |
| Loop | Logic & Data | Runs 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_documentslists the files uploaded with the run. Once a node asks for them as text,documentsholds 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:
| Field | Node | Syntax |
|---|---|---|
| Prompt Template | AI Agent | Template — {{variable_name}} |
| Output Template | End | Template — {{variable_name}} |
| Expression | Set State | CEL — bare variable_name |
| Expression | Condition | CEL — 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>_outputtoo; set this to something meaningful, likesummaryortranslated_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 daysD#is the document’s position in the run’sinput_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:
| Key | Value |
|---|---|
id | D1, D2, … |
artifact_id, filename, mime_type | The document |
line_count | Number of lines |
tables | One {first_line, last_line, header} per table |
text | The 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.
| Goal | Variable | Value (CEL) |
|---|---|---|
| Initialize a counter | retry_count | 0 |
| Increment a counter | retry_count | retry_count + 1 |
| Copy a node’s output to a stable name | context | node_search_1_output |
| Build a string | formatted_response | "Analysis: " + analysis_result |
| Append to a list | history_list | history_list + [last_result] |
| Derive a boolean | needs_review | size(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_countto0before a loop, then set it toretry_count + 1inside 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, expressionnode_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 examplecategory == "refund"orquery.contains("invoice"). The variable can be a dotted path such asaudit.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.
| Goal | Expression |
|---|---|
| Route on a previous answer | node_classify_1_output == "refund" |
| Guard on retrieved context | size(context) > 0 |
| Combine checks | needs_review && retry_count < 3 |
| Match text | query.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 fromstate.jsonin its working directory (chat_historyis left out):import json state = json.load(open("state.json")) total = len(state["query"])The script can’t change the shared state. Editing
state.jsonhas 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 asA1from this run. The entrydocumentsstages no file: it puts every input document’s numbered text instate["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:
| Setup | Node output |
|---|---|
| No schema, the script created or changed files | A reference to the first captured artifact (artifact_id, version, mime_type, filename, …) |
| No schema, no files | {"artifacts": [], "status": "ok"} |
| JSON schema set | The 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,
itemby default. Prompt templates read{{ item.name }}; Set State and If / Else expressions readitem.name. It can’t be a reserved name such asquery,chat_history,input_documentsor 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 to2, 10, 30. A 429 or 503 that sendsRetry-Afterwaits 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-Afteris longer thanWORKFLOW_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>_outputcopy is left out ofstate_delta. - When they don’t, the event is a summary:
summarized: true, the output itself when it is small or elseoutput_preview(its first 500 characters) withoutput_truncated: true,output_bytes, andstate_keys_changedlisting the keys the node set. A very long key list is cut to fit and markedstate_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):
| Setting | The 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.