shareAI-lab/learn-claude-code · error · WorkflowInputError

args.changes must be a string

Error message

args.changes must be a string

What it means

The built-in sample_workflow validates its primary input: args.get('changes', '') must be a string, otherwise WorkflowInputError('args.changes must be a string') is raised before any agent runs. This is the workflow's own contract check — the runtime itself only requires args to be an object, so each workflow validates the shape of its specific fields.

Source

Thrown at s16_workflow_runtime/code.py:679

    "description": "Review changed files across dimensions, verify each finding",
    "phases": ["Review", "Verify"],
}

DIMENSIONS = ["correctness", "security", "performance", "style"]
DEMO_CHANGES = (
    "def load_user(user_id):\n"
    "    query = f\"SELECT * FROM users WHERE id = {user_id}\"\n"
    "    return db.execute(query).fetchone()\n"
)


async def sample_workflow(ctx, args):
    """pipeline over review dimensions (audit -> verify-each), then keep only the
    findings a verifier confirms. The plan is code, not a chat turn."""
    ctx.phase("Review")
    changes = args.get("changes", "")
    if not isinstance(changes, str):
        raise WorkflowInputError("args.changes must be a string")
    review_input = changes.strip() or "No change context was supplied."

    async def audit(_value, dimension, _idx):
        out = await ctx.agent(
            f"Review this change context for {dimension} issues. "
            "Report only issues supported by the supplied text.\n\n"
            f"{review_input}",
            schema=FINDINGS_SCHEMA, label=f"audit:{dimension}", phase="Review")
        return {"dimension": dimension, "findings": out["findings"]}

    async def verify(audited, dimension, _idx):
        ctx.phase("Verify")
        # Each finding is verified by its own adversarial subagent, concurrently.
        verdicts = await ctx.parallel([
            (lambda f=f: ctx.agent(
                f"Adversarially verify this {dimension} finding against the "
                "supplied change context.\n\n"
                f"Change context:\n{review_input}\n\n"

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Serialize the change context to a single string (join diffs, read file contents) before calling.
  2. Omit 'changes' entirely to fall back to the default empty string ('No change context was supplied.').
  3. Add the same isinstance check on the caller side before invoking the workflow.

Example fix

# before
args = {"changes": [diff_a, diff_b]}

# after
args = {"changes": "\n\n".join([diff_a, diff_b])}
Defensive patterns

Strategy: type-guard

Validate before calling

changes = args.get("changes", "")
if not isinstance(changes, str):
    args = {**args, "changes": "\n".join(map(str, changes['changes'] if isinstance(changes, list) else [changes]))}
await run_workflow("review", args=args)

Type guard

def valid_review_args(args) -> bool:
    return isinstance(args, dict) and isinstance(args.get("changes", ""), str)

Try / catch

try:
    await run_workflow("review", args=args)
except WorkflowInputError as e:
    if "args.changes must be a string" in str(e):
        args["changes"] = str(args["changes"])
        return await run_workflow("review", args=args)
    raise

Prevention

When it happens

Trigger: run_workflow('review', args={'changes': [list of diffs]}) or {'changes': 123} — any non-string value under the 'changes' key.

Common situations: Passing a diff/PR object or list of file changes instead of their serialized text; JSON clients coercing values; forgetting to stringify before calling the tool.

Related errors


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