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
- Serialize the change context to a single string (join diffs, read file contents) before calling.
- Omit 'changes' entirely to fall back to the default empty string ('No change context was supplied.').
- 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
- Stringify diffs/change context at the boundary (join lists, read files) before building args.
- Mirror each workflow's arg contract with a client-side schema check.
- Omit optional keys rather than sending nulls; defaults (e.g. changes='') are handled by the workflow.
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
- workflow args must be an object
- resume args do not match the original run
- workflow name must be a string
- todos must be a list
- todos[{index}] must be an object
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/ccd3481b4f9cefd7.
Report an issue: GitHub.