shareAI-lab/learn-claude-code · error · WorkflowInputError
workflow() nesting is one level only
Error message
workflow() nesting is one level only
What it means
ExecutionState.workflow() runs a registered workflow inline as a child of the current run, sharing journal/budget/limits, but only one level of nesting is allowed. The child ExecutionState is created with depth=self._depth + 1, and any workflow() call at depth >= 1 raises WorkflowInputError('workflow() nesting is one level only'). This keeps the run graph flat and budgets/locks comprehensible.
Source
Thrown at s16_workflow_runtime/code.py:525
"""BARRIER: run all thunks concurrently and fail if any thunk fails."""
return await asyncio.gather(*[thunk() for thunk in thunks])
async def pipeline(self, items, *stages):
"""Per-item staged flow, NO barrier between stages: item A can be in
stage 3 while item B is still in stage 1. Each stage gets
(prev_result, original_item, index). A throwing stage fails the workflow."""
async def run_item(item, idx):
value = item
for stage in stages:
value = await stage(value, item, idx)
return value
return await asyncio.gather(*[run_item(it, i) for i, it in enumerate(items)])
async def workflow(self, name, args=None):
"""Run a saved workflow inline as a child (one level), sharing this run's
journal + budget + agent counter."""
if self._depth >= 1:
raise WorkflowInputError("workflow() nesting is one level only")
if name not in WORKFLOWS:
raise WorkflowInputError(f"unknown workflow '{name}'")
meta, fn = WORKFLOWS[name]
child = ExecutionState(self.task, self.journal, self.runner, self.budget,
args or {}, depth=self._depth + 1,
limits=self._limits)
return await fn(child, args or {})
# -- Workflow Tool --
class WorkflowTool:
"""The Workflow tool. .call() validates meta, runs the permission check,
creates runId/taskId, registers a LocalWorkflowTask, and emits lifecycle
events while executing the script. It returns the result and task state and
supports resume."""
async def call(self, meta, script_fn, args=None, resume_from_run_id=None):
validate_meta(meta)View on GitHub (pinned to 985456f4ad)
Solutions
- Flatten the composition: inline the grandchild's logic (or extract it into plain helper functions) so no workflow() call happens inside a child.
- Run the two workflows sequentially as separate top-level runs, feeding the first run's result into the second's args.
- Move shared steps into a helper the parent calls via ctx.agent()/pipeline instead of a nested workflow().
Example fix
# before (inside child workflow)
async def child(ctx, args):
return await ctx.workflow("grandchild", args) # depth 1 -> raises
# after
async def child(ctx, args):
# inline the grandchild steps directly at this level
out = await ctx.agent(..., label="grandchild:step")
return out Defensive patterns
Strategy: validation
Validate before calling
def plan_nesting(workflow_fns, name) -> int:
# walk which workflows each fn calls via ctx.workflow(); reject depth > 1
depth = 0
for called in workflow_fns[name].calls_workflow():
depth = max(depth, 1 + plan_nesting(workflow_fns, called))
return depth
assert plan_nesting(workflow_fns, entry) <= 1 Try / catch
try:
await ctx.workflow("child", args)
except WorkflowInputError as e:
if "nesting" in str(e):
# flatten: run child as a separate top-level run
await run_workflow("child", args)
else:
raise Prevention
- Design workflows leaf-first: helpers are plain functions, only top-level entry points are registered.
- Enforce the one-level rule in review: grep for ctx.workflow( inside any workflow body other than entry points.
- Chain workflows by passing runIds/results between top-level runs, not by nesting.
When it happens
Trigger: A workflow function calls ctx.workflow('child', ...) when it is itself already running as a child of another workflow — i.e. a grandchild composition attempt.
Common situations: Refactoring shared logic into composable workflows and chaining three deep; copy-pasting a workflow that itself embeds another workflow.
Related errors
- token budget exceeded ({self._spent + n} > {self._total})
- agent() cap reached ({AGENT_CAP})
- Max 20 todos
- Max 20 todos allowed
- memory store is too large for one consolidation pass
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/4d5a60ec8ec6c808.
Report an issue: GitHub.