Workflows
Purpose: stitch agents together, define node order/deps, and choose final output.
Start Here
For local commands, configure the workspace environment
with ACTSPAN_PORTAL_DISABLED=1 in its .actspan.env. Run from that
workspace directory and keep the selected provider credentials configured.
Use a workflow when multiple actions, prompts, agents, or nested workflows must execute in a declared order and pass outputs between steps.
cd /path/to/actspan-examples/openai/data_enrichment
actspan graph workspace.yaml data_enrichment
actspan run workspace.yaml data_enrichment \
--input-text "Analyze NVIDIA (NVDA)"
Expected for this linear downloaded example: research precedes summarization and the workflow returns its configured final output. Inspect the actual step outcomes; other workflows can branch, skip nodes, pause or resume.
Review a portal-reported approval
When a native CLI workflow reports an approval pause to a matching control API,
Actspan publishes a bounded review of the saved checkpoint. The portal shows the
recorded research, available source links, and reviewer decision together.
Metadata-only synchronization omits the research content. Sources shown are
recorded links; displaying a link does not verify the underlying facts.
An authenticated human with the configured workspace approver role or
workflow:approve permission can approve or reject. The server records that
identity. A local --actor label or a developer service account cannot supply
reviewer authority. Managed portal approval actions use the same reviewer check.
For an already reported pause with a supported configuration binding, publish its saved review without rerunning research:
actspan continue --thread-id <thread-id> --publish-review --format json
After the reviewer decides in the portal, continue from the original workspace:
actspan continue --thread-id <thread-id> --portal-decision --format json
The portal records the decision; this command executes the saved continuation and links its runtime record to the original paused run. Changed saved research, source, policy, variables, environment, or conflicting decisions block continuation on this path. POSIX thread locking prevents concurrent local continuations; completed claims cannot be reused for another execution.
Use an explicit condition after the approval node to choose what rejection
does. Approval nodes record either decision; they do not implicitly cancel all
downstream steps. The data-enrichment demonstration routes rejection to a
terminal handoff and skips summarization.
Local workflows with portal reporting disabled retain local approval labels. SDK checkpoint methods reject continuation of CLI-owned portal threads: use the original CLI so the shared execution history is retained. Thread state stays at its originating runtime; this is not remote execution of the CLI workspace or independent attestation of every external call.
Configuration changes while approval is pending
Starting with core 0.1.2, native CLI and SDK approvals bind to the effective
workspace configuration, including agent permissions, prompts, provider/model
selection, workflow rules, declared local tool source and provider lock. A change
returns APPROVAL_CONFIGURATION_CHANGED before approval consumption or tool
execution. Start a new run and request a fresh review for the changed behavior.
Pending approvals created by earlier releases lack this binding and return
APPROVAL_CONFIGURATION_UNBOUND. Start a new run instead of editing saved state
to reuse an old decision. Credential-value rotation and YAML comments/map order
do not invalidate a binding; behavior-bearing values and declared dependency
bytes do. Saved workflow inputs remain protected separately. Published 0.2.0
has a package-resolution exception;
its imported helpers are not bound. Explicit dependency manifests are a
source-only addition pending a separately qualified release.
This check protects the mediated continuation. It does not attest arbitrary Python imports, remote tool implementation or a caller who rewrites local state.
Supported authoring forms
The public 0.2.0 data-enrichment download uses mapping-form workflows with
steps. Keep that supplied form for its walkthrough. steps is normalized into
nodes; explicit nodes is useful when showing conditions and checkpoints.
Both forms are accepted. Choose one form per workflow rather than combining
them; the syntax below is a partial reference, not a standalone starter.
Syntax
workflows:
- name: <workflow_name>
id: <optional_id>
description: "..."
tags: [prod, routing]
metadata: { owner: "eng" }
tool_policy:
allow: [lookup_customer_profile, search_policy_kb]
deny: [local_shell]
nodes:
- id: <node_id>
kind: agent | prompt | tool | condition | wait_for_user | approval | handoff
ref: <ref_name> # for kind=agent/prompt/tool
deps: [<node_ids>] # optional
allow_skipped_inputs: true # optional explicit fan-in / join behavior
tool_policy:
allow: [search_policy_kb]
approval_required: true
inputs:
input_text: "${INPUT_TEXT}"
- id: route
kind: condition
deps: [triage_node]
expr: "${triage_node.output_parsed.has_all_details} == true"
then: launch_node
else: get_data_node
- id: wait_customer
kind: wait_for_user
deps: [launch_node]
prompt: "Please share your order number."
inputs:
case_id: "${launch_node.output_parsed.case_id}"
- id: manager_gate
kind: approval
deps: [launch_node]
prompt: "Approve refund over threshold?"
reason: "${launch_node.output_parsed.policy_reason}"
- id: human_handoff
kind: handoff
deps: [manager_gate]
prompt: "Escalate to senior support."
reason: "specialist_review"
output:
from: "${some_node.output}"
format: json | text
Field notes:
name(required): workflow identifier.id(optional): stable identifier separate from name.tags,metadata: optional annotations.nodes: DAG of steps;depsgate execution order.kind:agent/prompt/tool: runs the referenced definition.condition: evaluatesexprand runs onlythenorelse, skipping the other.wait_for_user: pauses the workflow in a resumablewaiting_for_userstate and persists thread state.approval: pauses the workflow inpending_approvaluntil a human approves or rejects the checkpoint.handoff: pauses the workflow inpending_handoffuntil a human marks the handoffcompleteorterminal.
- Agent nodes with
memory:policy use persisted thread transcript as bounded conversation context and updatethread_state.memory_snapshot.by_agent. inputs: templated values referencing workflow input or previous node outputs.allow_skipped_inputs: explicit fan-in contract. If true, the node may execute even when one or more upstream branch dependencies were skipped.tool_policy: runtime tool governance foragent,prompt, ortoolnodes and optional workflow-wide defaults.allow: permitted tool names. If a node definesallow, it overrides the workflow-levelallow.deny: denied tool names. Node and workflowdenylists are merged.approval_required: if true, the node may only execute tools after an approved checkpoint exists in the thread audit trail.
prompt: optional checkpoint message emitted in the pause payload.reason: optional checkpoint reason metadata forapprovalandhandoffnodes.edges: optional explicit graph; otherwise deps/expressions imply order.output.from: expression or node output reference that becomes the workflow result;formathint (text/json), optionaljson_schema(future validation hook).steps: supported readable authoring form, normalized into the node graph.
Execution model (current engine behavior):
- DAG resolution: node dependencies are topologically ordered; execution fails loud if a node output cannot be parsed/validated.
- State per node: each node’s result is stored and addressable via
nodes.<id>.output(single) ornodes.<id>.outputs(collected map results). - Branch semantics: a
conditionselects one branch root and recursively marks the non-selected subtree asskipped. Skipped nodes stay addressable in context withoutput: null,skipped: true, andtelemetry.status: skipped. String interpolation of skipped outputs resolves to an empty string, while structured/path lookups resolve tonull. Useallow_skipped_inputs: trueon explicit join nodes that should fan in partially skipped branches. - Parallel where possible: independent branches and mapped items (
map.over) run concurrently; dependent nodes run sequentially (e.g., A→B→C means C waits for B). - Output formatting:
output: { from: <node>.output, format: json }standardizes the final result; per-node validation/repair is applied so bad outputs do not silently propagate.
Execution examples:
- Linear (A→B→C):
bdeclaresdeps: [a],cdeclaresdeps: [b]; they run in order. Useinputs: { foo: "${nodes.a.output}" }to pass data forward. - Branch + fan-in:
bandcboth depend ona; they run in parallel. A downstreamdwithdeps: [b, c]waits for both and can readnodes.b.outputandnodes.c.output. - Fan-out + collect: with
map.over: "${INPUT_LIST}", each item is processed in parallel and collected innodes.<id>.outputs; a later node can consume that array to summarize or aggregate. - Pause + resume seam:
wait_for_user,approval, andhandoffstore athread_id, return a checkpoint payload, and stop execution so a later human action can continue the conversation. Preferred CLI shape:actspan workflow invoke <workspace> <workflow> --input-text ...for the first turn, thenactspan workflow invoke --thread-id ... --input-text ...for resumed turns. Approval actions useactspan workflow approve|reject --thread-id ..., and handoff actions useactspan workflow handoff --thread-id ... --resolution complete|terminal. For an interactive shell over the same runtime, useactspan workflow chat <workspace> <workflow>. Follow the public approval guide for decision and continuation commands, or the planning example for a user-response pause.
Approval must route the decision
An approval records a decision. Put a condition after it; rejection must take a
terminal branch, and the protected tool must require an approved checkpoint.
This excerpt requires the upstream assess node, named agents, tool implementation
and policy from the
complete refund example; it is not a complete
workspace on its own.
nodes:
- id: manager_gate
kind: approval
deps: [assess]
prompt: Approve the quoted synthetic refund of ${assess.output_parsed.amount_usd} USD?
inputs:
assessment: ${assess.output}
- id: choose_decision
kind: condition
deps: [manager_gate]
expr: '${manager_gate.output_parsed.decision} == "approve"'
then: reviewed_payment
else: rejected
- id: reviewed_payment
kind: agent
ref: reviewed_refund_payer
deps: [choose_decision]
tool_policy:
allow: [issue_reviewed_refund]
approval_required: [issue_reviewed_refund]
inputs:
input_text: ${assess.output}
- id: rejected
kind: agent
ref: review_declined_reply
deps: [choose_decision]
inputs:
input_text: ${assess.output}
tool_policy:
allow: []
deny: [issue_auto_refund, issue_reviewed_refund]
The referenced rejection-reply agent in that example has no payment tools. Inspect the ledger to establish absence of a write. Follow the refund example for its fully wired terminal-result branch, quoted amount and receipt checks.
Examples
Rewrite + classify (linear):
workflows:
- name: rewrite_and_classify
nodes:
- id: rewrite_node
kind: agent
ref: rewrite_agent
inputs: { input_text: "${INPUT_TEXT}" }
- id: classify_node
kind: agent
ref: classify_agent
deps: [rewrite_node]
inputs: { input_text: "${rewrite_node.output_parsed.rewritten}" }
output: { from: classify_node.output, format: json }
Route based on triage (condition):
nodes:
- id: triage_node
kind: agent
ref: triage
inputs: { input_text: "${INPUT_TEXT}" }
- id: route
kind: condition
deps: [triage_node]
expr: "${triage_node.output_parsed.has_all_details} == true"
then: launch_node
else: get_data_node
- id: launch_node
kind: agent
ref: launch_helper
deps: [route]
inputs: { triage_summary: "${triage_node.output_parsed.summary}" }
- id: get_data_node
kind: agent
ref: get_data
deps: [route]
inputs:
triage_summary: "${triage_node.output_parsed.summary}"
missing_fields: "${triage_node.output_parsed.missing_fields}"
- id: final_message
kind: agent
ref: final_message_agent
deps: [launch_node, get_data_node]
allow_skipped_inputs: true
inputs:
input_text: |
launch=${launch_node.output}
get_data=${get_data_node.output}
output: { from: final_message.output, format: text }
Prompt + agent + tool node:
workflows:
- name: prompt_then_tool
nodes:
- id: ask
kind: prompt
ref: rewrite_prompt
inputs: { input_text: "${INPUT_TEXT}" }
- id: compute
kind: tool
ref: math_tool # tool defined under top-level tools
deps: [ask]
inputs: { a: 7, b: 6 }
- id: format
kind: agent
ref: format_agent
deps: [compute]
inputs: { input_text: "${compute.output}" }
output: { from: format.output, format: json }
Validation
actspan validate workspace.yamlactspan preview workspace.yaml --format jsonto see plans and cost estimates.
Common errors
- Unknown
refordeps. - Output reference points to a non-existent node or field.
- Condition
exprreferences missing paths.
How it fits together (Prompts vs Agents vs Workflows)
- Prompts: Define a reusable provider-operation template (messages, model params, response_format, tools). They are the reusable request shapes.
- Agents: Wrap prompts (structured) or instructions (assistant) with runtime behavior (tools, runtime limits, memory). Agents are the runnable units that workflows call.
- Workflows: Orchestrate multiple nodes (agents/prompts/tools/conditions) into a DAG, handle branching, and pick the final output. This is the pipeline layer that stitches calls together.