Workflow engine

A workflow chains agent calls, tool calls and human confirmations into one graph: the definition is stored as node and edge JSON, the graph is validated before a run, and each node leaves its own record. One key fact: execution is sequential, not parallel — a single scoped DbContext is shared on purpose, trading parallelism for simplicity and consistency.

Definition Nodes / Edges as JSON Version bumps per update Graph validation needs Start and End edges resolve · agent needs AgentId Run writes GraphSnapshot 100 iterations · 20 visits / node Node records WorkflowRunNode: Pending / Running Succeeded / Failed / Skipped Outlets SSE: 9 event types Human review: /approve returns approved / rejected Node executors (10 kinds) start · agent · condition · loop · parallel / merge tool · human · subworkflow · end
Definition, validation, run and node records are all persisted; the run snapshots the graph so later edits cannot affect an in-flight execution.

Definition model

FieldNotes
Name · Description · SortOrder · IsEnabledMetadata and list ordering
Nodes · EdgesStored as JSON strings; node type constants: start / agent / condition / loop / parallel / merge / tool / human / subworkflow / end
InputSchema · VariablesInput contract and variable declarations
VersionStarts at 1 and increments on every update
Node ConfigPer-node JSON configuration, see the executor table

Endpoints

EndpointPurpose
GET /api/workflows · GET /api/workflows/{id}List / detail
POST /api/workflows · PUT /api/workflows/{id} · DELETE /api/workflows/{id}Create / update / delete (nodes and edges are serialised to JSON and the version increments)
POST /api/workflows/{id}/duplicateCopy a definition with a localised copy suffix
GET /api/workflows/{id}/validateValidate the graph without running it
POST /api/workflows/{id}/runRun once and return the result
POST /api/workflows/{id}/run/streamRun with an SSE stream
POST /api/workflows/{id}/run/topic/{topicId}/streamStreaming run that also writes the process into a chat topic
GET /api/workflows/{id}/runs · GET /api/workflows/runs/{runId}Run history / one run with its node records
POST /api/workflows/runs/{runId}/approveHuman review callback: { nodeId?, approve }

Graph validation rules

  • At least one start and one end node.
  • Both endpoints of every edge must exist.
  • No orphan nodes other than start (everything must be reachable).
  • agent nodes must carry an AgentId (a project agent preset).

Validation runs again before execution, so "saved fine but fails validation at run time" usually means the definition changed underneath or an edge was removed.

Node executors

NodeConfig and behaviour
startStores the raw input as start.input; if the input is a JSON object, each field also becomes a variable
agentinstruction / modelId / toolNames / mcpServerIds / toolApprovals / maxIterations / maxToolCallsPerTurn; upstream output is injected as input, falling back to the run input; SessionId = workflow-{runId}-{nodeId}; executed through the agent loop and writes thinking and toolCalls; usage source workflow
condition{ branches: [{ handle, expression }], defaultHandle }; the first matching branch determines the outgoing handle
loop{ maxIterations, exitCondition }; the first pass enters body, the exit condition or the iteration cap leaves through exit
tool{ toolName, argumentsTemplate }; calls a built-in or MCP tool without an LLM round trip
human{ prompt, timeoutSeconds }; approval returns the approved handle, rejection rejected
subworkflow{ subWorkflowId, inputTemplate }; recursion depth capped at 5
parallel / mergeA node with multiple outgoing edges fans out automatically; branch results are collected and merged at the common successor
endRenders outputTemplate when configured, otherwise takes the first non-empty upstream output, falling back to the run input
"Parallel" here is structurally parallel but sequentially executed: branches run one after another and then merge, sharing one scoped DbContext. That explains two behaviours: agent calls in branches are serial, and a failure in any branch aborts the whole run.

Runs and limits

ItemValue / behaviour
Run recordWorkflowRun: Status (Pending / Running / Succeeded / Failed / Cancelled) · Input · Output · GraphSnapshot · ChatTopicId · Error · TotalIterations
Node recordWorkflowRunNode: NodeId · NodeType · Status · Input · Output · Error · Iterations
Total iteration capMaxTotalIterations = 100; exceeding it throws and stops the run
Per-node visit capMaxNodeVisits = 20; exceeding it throws (runaway loop guard)
CancellationOperationCanceledException → status Cancelled
Other exceptionsStatus Failed with the message in Error

Human review and tool approvals

  • A human node pushes a waiting-for-approval event over SSE and the callback comes back through /approve.
  • Node-level toolApprovals win; otherwise the run-level global mode fills the gap.
  • How tool approvals interact with permission modes is covered in Permission modes & approvals, and the agent node's own loop in Agent loop.

SSE events

EventMeaning
run_started · run_completed · run_failedRun lifecycle
node_started · node_completed · node_failedPer-node progress and failures
human_approval_requiredA human node is waiting for the callback
agent_tool_callTool activity inside an agent node (the frontend can answer questions, plans or approvals)
run_resultFinal result payload

The full protocol (three frame shapes, unknown event handling) is in SSE event protocol; agent nodes still emit the [ERROR] fallback frame.

Relation to the task board

  • Kanban run records support Kind = Agent | Workflow with Trigger = Todo | Comment | Manual.
  • So a card can point at a workflow and the executor runs it once the card reaches the todo column, leaving process and result in the run record and comments (see Task board & automation).
  • Agent nodes inside a workflow do not reuse the kanban run record; they write their own node records.

Practice notes

  • Always give loops a maxIterations: the 20-visit cap per node is the last line of defence and it fails the run outright.
  • Agent session ids include run and node ids, so every run starts clean; share context across nodes through variables, not chat history.
  • tool nodes skip the LLM: ideal for fixed actions (run a command, fetch data) — cheaper and more predictable than letting a model decide.
  • Wire /validate into the editor flow; it catches the most common "saved but fails" cases such as a missing end node or an agent node without an agent.
  • Definition edits never affect an in-flight run: compare the run's GraphSnapshot when results look inconsistent.