{"record":{"id":"b3b649b1e086254c","repo":"shareAI-lab/learn-claude-code","slug":"workflow-name-must-be-a-string","errorCode":null,"errorMessage":"workflow name must be a string","messagePattern":"workflow name must be a string","errorType":"validation","errorClass":"WorkflowInputError","httpStatus":null,"severity":"error","filePath":"s16_workflow_runtime/code.py","lineNumber":747,"sourceCode":"}\n\n\ndef serialize_task(task):\n    return {\n        \"taskId\": task.task_id,\n        \"taskType\": \"local_workflow\",\n        \"runId\": task.run_id,\n        \"workflowName\": task.meta[\"name\"],\n        \"status\": task.status,\n        \"usage\": dict(task.usage),\n        \"progress\": list(task.progress),\n    }\n\n\nasync def run_workflow(name, args=None, resume_from_run_id=None):\n    \"\"\"Model-facing adapter: resolve trusted code from the host registry.\"\"\"\n    if not isinstance(name, str):\n        raise WorkflowInputError(\"workflow name must be a string\")\n    if name not in WORKFLOWS:\n        raise WorkflowInputError(f\"unknown workflow '{name}'\")\n    if args is not None and not isinstance(args, dict):\n        raise WorkflowInputError(\"workflow args must be an object\")\n    meta, script_fn = WORKFLOWS[name]\n    out = await WorkflowTool().call(\n        meta,\n        script_fn,\n        args=args,\n        resume_from_run_id=resume_from_run_id,\n    )\n    return {\n        \"launched\": out[\"launched\"],\n        \"result\": out[\"result\"],\n        \"task\": serialize_task(out[\"task\"]),\n    }\n\n","sourceCodeStart":729,"sourceCodeEnd":765,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s16_workflow_runtime/code.py#L729-L765","documentation":"run_workflow is the model-facing adapter: it refuses non-string workflow names with WorkflowInputError('workflow name must be a string') before touching the trusted WORKFLOWS registry. Type-checking happens before the registry lookup, so an int/None/list name never reaches the name-matching code.","triggerScenarios":"run_workflow(name=None), run_workflow(name=123), or any non-str value arriving from a JSON tool call whose schema failed to enforce the name's type.","commonSituations":"Tool-call arguments loosely typed (model emitting null for a forgotten field); programmatic callers passing an enum/object; schema drift between client and host.","solutions":["Pass the workflow name as a plain string literal matching a WORKFLOWS key.","Tighten the tool's input schema (type: string) so the model cannot emit other types.","Guard in the caller: isinstance(name, str) before invoking."],"exampleFix":"# before\nawait run_workflow(name=workflow_obj)  # an object/None\n\n# after\nawait run_workflow(name=\"review\")","handlingStrategy":"type-guard","validationCode":"if not isinstance(name, str) or not name:\n    raise TypeError(f\"workflow name must be a non-empty string, got {name!r}\")\nawait run_workflow(name, args)","typeGuard":"def is_workflow_name(name) -> bool:\n    return isinstance(name, str) and bool(WORKFLOW_NAME_RE.match(name))","tryCatchPattern":"try:\n    await run_workflow(name, args)\nexcept WorkflowInputError as e:\n    if \"must be a string\" in str(e):\n        name = str(name)  # or reject with a clear message upstream\n        return await run_workflow(name, args)\n    raise","preventionTips":["Declare the tool parameter as type:string in the schema the model sees.","Keep workflow names as shared string constants in the caller.","Validate name type and shape (WORKFLOW_NAME_RE) before launching."],"tags":["workflow","type-validation","tool-input","api"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}