Liking cljdoc? Tell your friends :D

Workflow IR

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.

Purpose

The workflow IR exists to make the runtime independent of authored syntax.

It gives execution one canonical model for:

  • step identity
  • execution form
  • control flow
  • judge execution
  • data references
  • session construction
  • delegated boundaries
  • step-local outputs
  • yielded values
  • workflow result composition

The runtime should execute IR, not raw authored workflow documents.

Design properties

The IR should have these properties:

  • Canonical — one execution model for all authored workflow files
  • Explicit — execution form, references, outputs, and yielded value are visible in data
  • Typed by tag — execution form and yielded-value semantics use explicit :type discrimination
  • Observable — runtime can record inspectable effective boundary inputs for each step form
  • Small — only execution-relevant concepts belong here

IR overview

A 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.

Step forms

The IR has three step execution forms:

  • invoke
  • session
  • delegate

Each step has exactly one execution :type.

Illustrative shape:

{:name "discover"
 :type :invoke
 ...}

Common step fields

All step forms share these conceptual fields:

  • :name
  • :type
  • optional :outputs
  • optional :yields
  • optional :judge
  • optional :on
  • optional :max-iterations

The IR should validate execution-specific fields according to step type.

Important alignment rule:

  • the authored target grammar hoists execution-specific fields directly onto the step
  • the IR groups those fields under one execution-specific key such as :invoke, :session, or :delegate

This keeps authored syntax compact while giving runtime one explicit normalized place for execution payload.

Invoke step

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}}

Invoke semantics

  • :operation is a canonical runtime operation id
  • :args is a fully normalized named-argument map
  • the operation runs without child-session construction
  • canonical machine-readable output is :data
  • optional human-readable output is :summary
  • optional debug/result envelope output is :result

Session step

A 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 semantics

  • :session contains the effective child-session construction data
  • :contributions are ordered and preserved as authored
  • the assembled result of contributions is the child-session conversation
  • canonical first-cut session output is :final-llm-reply
  • transcript output is optional but normalized when present

Delegate step

A 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}}

Delegate semantics

  • :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
  • the delegated workflow's local :workflow-input becomes the rendered prompt string
  • the delegated workflow's local :workflow-original is rebound for the delegated invocation
  • by default, the step yields the delegated workflow's yielded value unchanged
  • first cut does not re-export the callee workflow's step-local output surfaces through downstream {:step ... :output ...} refs against the delegate step itself
  • first cut does not use delegated session overrides; delegation is a workflow boundary, not a child-session customization surface

Outputs

The IR separates execution from output surfaces.

Each step may expose step-local outputs by logical output key.

Illustrative common output keys:

  • invoke step: :data, :summary, :result
  • session step: commonly :final-llm-reply, :transcript, :result
  • delegate step: no first-cut step-local outputs; only explicit delegate-local outputs such as a later justified debug/result surface if intentionally added

The :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.

Yielded value

The IR distinguishes:

  • step-local outputs addressable through :output
  • the step's resulting value as a whole, modeled through :yields

Yield forms

The 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:

  • yield the called workflow's yielded value unchanged

This avoids redundantly restating the delegated workflow's yield form at every delegating callsite.

Default yield rules

When omitted in authored input, compiler-side normalization must supply defaults before the runtime-owned IR validation boundary:

  • invoke step -> {:type :data :data :data}
  • session step -> {:type :text :text :final-llm-reply}
  • delegate step -> {: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:

  • deterministic/invoke steps yield their canonical machine-readable :data
  • inline session steps yield their canonical terminal text output key :final-llm-reply
  • delegated steps yield the callee's yielded value unchanged

Control flow

Control flow is orthogonal to execution form.

The IR uses:

  • :judge
  • :on
  • :max-iterations

Illustrative shape:

{:judge {...}
 :on {"APPROVED" {:goto :done}
      "REVISE" {:goto "build" :max-iterations 3}}
 :max-iterations 5}

Routing rules

  • a judge produces one logical outcome value
  • :on maps that outcome to a transition directive
  • normalized IR requires :on when :judge is present, and requires :judge when :on is present
  • if a selected transition goes to :done, the parent step's yielded value becomes the workflow result
  • a judge routes; it does not replace the parent step's yielded value

Judge forms

The IR should normalize judge execution mode explicitly.

Current executed runtime support:

  • :type :llm

Documented-but-not-yet-executed schema shape:

  • :type :invoke

Runtime support note

The 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 path
  • task 083 proves invoke-step execution plus shared judged routing at the workflow level, but does not prove invoke-typed judge execution specifically

LLM judge

Illustrative shape:

{:type :llm
 :session {:model "gpt-5.4"
           :contributions [...]} 
 :projection {:type :tail
              :turns 4
              :tool-output false}}

Invoke judge

Illustrative shape:

{:type :invoke
 :invoke {:operation "workflow/classify-result"
          :args {:result {:from {:step "build" :output :data}}}}}

Judge outcome contract

All judge forms normalize to one logical outcome value.

That outcome:

  • may be a string or keyword
  • is matched exactly against the keys in :on
  • is case-sensitive for strings
  • does not auto-coerce between strings and keywords

Source references

The IR uses one shared source-spec language for:

  • invoke args
  • source contributions
  • template vars
  • delegated context
  • delegated prompt-string template vars
  • judge args where applicable

Current runtime ownership note:

  • canonical runtime materialization of these refs/specs now lives in components/agent-session/src/psi/agent_session/workflow_source_resolution.clj
  • compiler/authoring seams may translate authored syntax into IR-compatible refs/specs, but they should not re-encode divergent runtime resolution semantics

Source spec

Illustrative 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.

Source refs

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.

Meaning of refs

  • :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 s

Current implementation note:

  • canonical normalized IR currently admits :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 time

Contributions

Session construction and delegated context reuse normalized contribution items.

Source contribution

{:type :source
 :from source-ref
 source-projection?}

Template contribution

{:type :template
 :text string
 :vars {string source-spec}*}

Rules:

  • author order is preserved
  • template vars use the same source-spec language as invoke args
  • unresolved vars are first-cut errors
  • template rendering must produce deterministic text from resolved values

Delegated prompt-string

Delegate :prompt-string may be represented in IR either as:

  • a literal final string, or
  • a template contribution-like renderer that will deterministically render to a final string before invocation

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.

Workflow result composition

The workflow result is the yielded value of the step whose chosen transition reaches :done.

Rules:

  • direct step -> :done means that step's yielded value becomes the workflow result
  • judge-selected transition -> :done still returns the parent step's yielded value
  • delegated steps normally return the delegated workflow's yielded value unchanged

Suggested documentation grammar

This 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

Recommended use

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close