This document defines the normalized workflow IR for deterministic workflow steps.
The workflow IR is the canonical runtime execution model for authored workflow
files compiled from doc/workflow-grammar.md.
It is intentionally not a user-facing authoring grammar. It is the normalized execution boundary used by runtime execution.
The workflow IR exists to make the runtime independent of authored syntax.
It gives execution one canonical model for:
The runtime should execute IR, not raw authored workflow documents.
The IR should have these properties:
:type discriminationA normalized workflow IR is a map with ordered steps.
Illustrative top-level shape:
{:version :workflow-ir/v1
:steps [ir-step+]}
The normalized IR boundary requires at least one step. Empty workflows are invalid IR and should be rejected before execution.
This IR is the compiled execution model, not an authored workflow surface. It is intentionally close to the workflow grammar while remaining free to carry runtime-oriented normalization detail.
The ordered :steps vector is the canonical authored/program order.
The runtime may derive a name index or graph index as needed, but those are execution-time conveniences rather than authored meaning.
The IR has three step execution forms:
Each step has exactly one execution :type.
Illustrative shape:
{:name "discover"
:type :invoke
...}
All step forms share these conceptual fields:
:name:type:outputs:yields:judge:on:max-iterationsThe IR should validate execution-specific fields according to step type.
Important alignment rule:
:invoke, :session, or :delegateThis keeps authored syntax compact while giving runtime one explicit normalized place for execution payload.
An invoke step executes a deterministic operation.
Illustrative shape:
{:name "discover"
:type :invoke
:invoke {:operation "github/search-issues-by-label"
:args {:repo {:from :workflow-input :path [:repo]}
:labels {:from :workflow-input :path [:labels]}
:state "open"}}
:outputs {:data {:source :invoke/data}
:summary {:source :invoke/summary}
:result {:source :invoke/result}}
:yields {:type :data
:data :data}}
:operation is a canonical runtime operation id:args is a fully normalized named-argument map:data:summary:resultA session step constructs and runs a child session inline.
Illustrative shape:
{:name "report"
:type :session
:session {:model "gpt-5.4"
:tools ["read" "bash"]
:skills ["issue-feature-triage"]
:contributions [{:type :source
:from :workflow-original}
{:type :template
:text "Review these issues:\n\n{{issues}}"
:vars {"issues" {:from {:step "discover" :output :data}
:path [:issues]}}}]}
:outputs {:final-llm-reply {:source :session/final-llm-reply}
:transcript {:source :session/transcript}
:result {:source :session/result}}
:yields {:type :text
:text :final-llm-reply}}
:session contains the effective child-session construction data:contributions are ordered and preserved as authored:final-llm-replyA delegate step invokes another workflow through an explicit workflow boundary.
Illustrative shape:
{:name "report-call"
:type :delegate
:delegate {:target "builder"
:prompt-string {:type :template
:text "Review these issues:\n\n{{issues}}"
:vars {"issues" {:from {:step "discover" :output :data}
:path [:issues]}}}
:context [{:type :source
:from :workflow-original}
{:type :source
:from {:step "discover" :output :data}
:path [:issues]}]}
:yields {:type :delegated}}
:target resolves to a named workflow definition:prompt-string must render to a final string before invocation:context is ordered forwarded material and is optional:workflow-input becomes the rendered prompt string:workflow-original is rebound for the delegated invocation{:step ... :output ...} refs against the delegate step itselfThe IR separates execution from output surfaces.
Each step may expose step-local outputs by logical output key.
Illustrative common output keys:
:data, :summary, :result:final-llm-reply, :transcript, :resultThe :outputs map should describe what logical output keys exist for a step and, at minimum, give runtime a canonical local meaning for each key.
The exact internal value of each :outputs entry may evolve, but the key-space should be stable for runtime reference and validation.
If a local :yields form names an output key (for example {:type :text :text :final-llm-reply} or {:type :data :data :data}), that output key must be declared in the same step's :outputs map at the normalized IR boundary.
The IR distinguishes:
:output:yieldsThe first-cut yielded-value union is:
{:type :data
:data output-key}
{:type :text
:text output-key}
{:type :error
:reason keyword
:message string
:details? map}
For delegate steps, the normal behavior is compositional delegation:
{:type :delegated}
Meaning:
This avoids redundantly restating the delegated workflow's yield form at every delegating callsite.
When omitted in authored input, compiler-side normalization must supply defaults before the runtime-owned IR validation boundary:
{:type :data :data :data}{:type :text :text :final-llm-reply}{:type :delegated}The IR validator treats missing :yields as invalid normalized IR rather than filling defaults locally.
This aligns with the target grammar's preferred defaults:
:data:final-llm-replyControl flow is orthogonal to execution form.
The IR uses:
:judge:on:max-iterationsIllustrative shape:
{:judge {...}
:on {"APPROVED" {:goto :done}
"REVISE" {:goto "build" :max-iterations 3}}
:max-iterations 5}
:on maps that outcome to a transition directive:on when :judge is present, and requires :judge when :on is present:done, the parent step's yielded value becomes the workflow resultThe IR should normalize judge execution mode explicitly.
Current executed runtime support:
:type :llmDocumented-but-not-yet-executed schema shape:
:type :invokeThe current runtime judge execution path is prompt/session-based only.
components/agent-session/src/psi/agent_session/workflow_judge.clj executes
LLM judges by creating a judge child session and prompting it. There is not yet
an executed runtime branch for :judge {:type :invoke ...} through the
deterministic operation registry.
Therefore:
:judge {:type :llm ...} is part of the current executed IR contract:judge {:type :invoke ...} is currently a schema/documentation shape for a
future follow-on, not a landed executed runtime pathIllustrative shape:
{:type :llm
:session {:model "gpt-5.4"
:contributions [...]}
:projection {:type :tail
:turns 4
:tool-output false}}
Illustrative shape:
{:type :invoke
:invoke {:operation "workflow/classify-result"
:args {:result {:from {:step "build" :output :data}}}}}
All judge forms normalize to one logical outcome value.
That outcome:
:onThe IR uses one shared source-spec language for:
Current runtime ownership note:
components/agent-session/src/psi/agent_session/workflow_source_resolution.cljIllustrative shape:
{:from source-ref
:path [:issues]}
or:
{:from source-ref
:projection {:type :tail :turns 4 :tool-output false}}
A source-spec must not contain both :path and :projection in the first cut.
Illustrative source refs:
:workflow-input
:workflow-original
{:step "discover" :output :data}
{:step "discover" :yield :data}
These align with the workflow grammar's shared data-reference model and form the runtime boundary.
:workflow-input -> current workflow invocation input value:workflow-original -> current workflow invocation original request surface{:step s :output k} -> step-local output surface k from prior step s{:step s :yield f} -> yielded-value field f from prior step sCurrent implementation note:
:workflow-input, :workflow-original, prior-step :output, and prior-step :yield:workflow-runtime is not a canonical IR :from source-ref and target-authored definitions using it are rejected at validation timeSession construction and delegated context reuse normalized contribution items.
{:type :source
:from source-ref
source-projection?}
{:type :template
:text string
:vars {string source-spec}*}
Rules:
Delegate :prompt-string may be represented in IR either as:
Recommended first-cut shape keeps authored intent visible until boundary rendering:
:string
or
{:type :template
:text string
:vars {string source-spec}*}
Before actual delegated execution, runtime should materialize this to a final string.
The workflow result is the yielded value of the step whose chosen transition reaches :done.
Rules:
:done means that step's yielded value becomes the workflow result:done still returns the parent step's yielded valueThis section gives a compact documentation grammar for the normalized IR.
workflow-ir ::= {:version :workflow-ir/v1
:steps [ir-step+]}
ir-step ::= invoke-ir-step | session-ir-step | delegate-ir-step
invoke-ir-step ::= {:name step-name
:type :invoke
:invoke invoke-spec
outputs?
yields?
control-flow*
compat?}
session-ir-step ::= {:name step-name
:type :session
:session session-spec
outputs?
yields?
control-flow*
compat?}
delegate-ir-step ::= {:name step-name
:type :delegate
:delegate delegate-spec
outputs?
yields?
control-flow*
compat?}
invoke-spec ::= {:operation operation-id
:args {keyword (literal | source-spec)}*}
session-spec ::= {:model? model-selection-spec
:tools? [tool-id*]
:skills? [skill-id*]
:contributions [contribution+]
session-extension*}
delegate-spec ::= {:target workflow-name
:prompt-string (string | template-contribution)
:context? [source-contribution*]}
control-flow ::= :judge judge-spec
| :on outcome-map
| :max-iterations pos-int
judge-spec ::= llm-judge | invoke-judge ;; invoke-judge is documented IR shape; current runtime executes llm-judge only
llm-judge ::= {:type :llm
:session judge-session-spec
:projection? projection}
judge-session-spec ::= {:model? model-selection-spec
:tools? [tool-id*]
:skills? [skill-id*]
:contributions [contribution+]}
invoke-judge ::= {:type :invoke
:invoke invoke-spec}
outcome-map ::= {outcome transition-map}+
transition-map ::= {:goto goto-target
:max-iterations? pos-int}
goto-target ::= :next | :previous | :done | step-name
outputs ::= {:outputs {output-key output-spec}+}
output-spec ::= {:source keyword
output-metadata*}
yields ::= {:type :data :data output-key}
| {:type :text :text output-key}
| {:type :error :reason keyword :message string :details? map}
| {:type :delegated}
contribution ::= source-contribution | template-contribution
source-contribution ::= {:type :source
:from source-ref
source-projection?}
template-contribution ::= {:type :template
:text string
:vars {var-name source-spec}*}
source-spec ::= {:from source-ref
source-projection?}
source-projection ::= :path path
| :projection projection
source-ref ::= :workflow-input
| :workflow-original
| {:step step-name :output output-key}
| {:step step-name :yield yield-field}
;; `:workflow-runtime` is intentionally not a canonical normalized IR source-ref.
output-key ::= keyword
yield-field ::= keyword
projection ::= map
compat ::= :compat map
step-name ::= string
workflow-name ::= string
operation-id ::= string
tool-id ::= string
skill-id ::= string
var-name ::= string
outcome ::= string | keyword
path ::= vector
literal ::= string | keyword | number | boolean | nil | vector | map
pos-int ::= integer
map ::= clojure-map
vector ::= clojure-vector
string ::= clojure-string
keyword ::= clojure-keyword
number ::= clojure-number
boolean ::= true | false
nil ::= nil
Use this document together with doc/workflow-grammar.md and
doc/workflow-grammar-concepts.md when changing workflow compilation,
validation, or runtime execution.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |