Skip to content

type="workflow" step bodies cannot reference parent agents #34

Description

@jn-dave

Summary

A type="workflow" step's inline workflow {} body is compiled as an independent sub-workflow that does not inherit the parent workflow's agent blocks. Body steps that set agent = "<name>" fail at compile time with unknown agent. There is also no syntactic way to declare agents inside the body. As a result, the workflow-body iteration feature (W10) only works for adapter-only steps (shell, noop); any agent-driven sub-loop is currently impossible.

This also limits workflow_file = "..." since externally loaded body files are compiled through the same path — they cannot reference parent agents either, and the body-spec schema doesn't accept agent declarations of their own. The natural mental model — "a workflow body is a workflow, possibly loaded from a file, that should accept any block type a top-level workflow accepts" — does not currently hold.

Repro

workflow "agent_in_body" {
  version       = "1"
  initial_state = "loop"
  target_state  = "done"

  agent "executor" {
    adapter = "copilot"
    config { model = "gpt-5.3-codex" }
  }

  step "loop" {
    type  = "workflow"
    count = 3

    workflow {
      step "execute" {
        agent = "executor"     # ← compile error: unknown agent "executor"
        input { prompt = "do work" }
        outcome "success" { transition_to = "_continue" }
        outcome "failure" { transition_to = "_continue" }
      }
    }

    outcome "all_succeeded" { transition_to = "done" }
    outcome "any_failed"    { transition_to = "done" }
  }

  state "done" { terminal = true }
}
$ bin/criteria validate repro.hcl
compile failed:
<nil>: step "execute": unknown agent "executor"

Root cause

Three related gaps:

  1. WorkflowBodySpec has no Agents field (and no Variables, no Permissions). workflow/schema.go:108-118 declares only Steps, States, Waits, Approvals, Branches, Outputs. Agents cannot be declared inside a workflow {} block, even when the block is loaded from an external file via workflow_file.

  2. buildBodySpec does not copy parent agents into the synthetic body Spec. workflow/compile_steps.go:421-469 constructs the body's Spec from wb.Steps/wb.States/etc. only. The enclosing workflow's agents are not threaded through. The same applies to variables and any other top-level workflow concept that should be visible to body steps.

  3. The compile-time agent-existence check therefore fails on body steps. workflow/compile_steps.go:60-63 checks g.Agents[sp.Agent] against the body's own (empty) agent map.

The two existing examples that exercise type="workflow" (examples/for_each_review_loop.hcl, workflow/testdata/iteration_workflow_step.hcl) both use adapter = "noop" exclusively — there is zero coverage for the agent-in-body path.

Impact

The natural pattern for a bounded agent review loop —

step "review_loop" {
  type  = "workflow"
  count = var.max_review_rounds

  workflow {
    step "execute" { agent = "executor" ... }
    step "verify"  { adapter = "shell"  ... }
    step "review"  { agent = "reviewer" ... }
    state "success" { terminal = true }
    state "abort"   { terminal = true; success = false }
  }

  outcome "all_succeeded" { transition_to = "commit" }
  outcome "any_failed"    { transition_to = "abort" }
}

— is the cleanest expression of "loop the executor↔reviewer cycle up to N times, exit early on approval, abort on failure." Without agent-in-body support, workstream automation has to fall back to flat explicit transitions and a hand-rolled shell-step counter, which obscures intent and hard-codes the bound across multiple state edges.

This blocks rewriting examples/workstream_review_loop.hcl to use the W10 iteration feature for its main agent loop. It also makes workflow_file = "..." substantially less useful: an external file cannot declare its own agents and cannot reference any from the loading workflow, so it's restricted to adapter-only chains.

Related

  • Add per-step visit limit to bound loops in workflows #12 — per-step max_visits is an alternative bounding mechanism for flat loops; it does not replace this gap because flat loops with a visit limit can't express "all_succeeded vs any_failed" aggregate routing the way type="workflow" + count does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions