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

  1. Flatten the composition: inline the grandchild's logic (or extract it into plain helper functions) so no workflow() call happens inside a child.
  2. Run the two workflows sequentially as separate top-level runs, feeding the first run's result into the second's args.
  3. 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

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


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/4d5a60ec8ec6c808. Report an issue: GitHub.