tinyhumansai/openhuman · warning
missing required field `op`
Error message
missing required field `op`
What it means
The `todo` board tool was invoked without a usable `op` string (todo.rs:121): args["op"] is absent or not a JSON string. op selects the dispatch arm add|edit|update_status|remove|replace|clear|list; an unknown-but-present op string is a different, soft ToolResult::error listing the expected ops.
Source
Thrown at src/openhuman/agent/tools/todo.rs:121
"cards": {
"type": "array",
"description": "Full card list for op=replace.",
"items": { "type": "object" }
}
},
"required": ["op"]
})
}
fn permission_level(&self) -> PermissionLevel {
PermissionLevel::None
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let op = args
.get("op")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing required field `op`"))?
.trim()
.to_string();
let location = current_location();
tracing::debug!(op = %op, thread_id = ?location.thread_id(), "[tool][todo] dispatch");
let result = match op.as_str() {
"add" => {
let content = required_string(&args, "content")?;
let mut patch = patch_from_args(&args)?;
if patch.approval_mode.is_none() {
patch.approval_mode = Some(default_task_approval_mode().await);
}
ops::add(&location, &content, patch).await
}
"edit" => {
let id = required_string(&args, "id")?;
let mut patch = patch_from_args(&args)?;View on GitHub (pinned to a221052e0d)
Solutions
- Include "op" as one of add|edit|update_status|remove|replace|clear|list
- Check for key-name drift (operation/action instead of op)
- Validate args against the tool schema (required: ["op"]) before dispatch
Example fix
// before
{ "content": "Ship the fix" }
// after
{ "op": "add", "content": "Ship the fix" } Defensive patterns
Strategy: type-guard
Validate before calling
const TODO_OPS = new Set(["add","edit","update_status","remove","replace","clear","list"]);
const a = args as Record<string, unknown>;
if (typeof a?.op !== "string" || !TODO_OPS.has(a.op)) {
throw new Error(`todo: 'op' must be one of ${[...TODO_OPS].join("|")}`);
} Type guard
const TODO_OPS = new Set(["add","edit","update_status","remove","replace","clear","list"]);
function isTodoDispatch(a: unknown): a is { op: string } {
const op = (a as Record<string, unknown>)?.op;
return typeof op === "string" && TODO_OPS.has(op);
} Try / catch
if let Err(e) = tool.execute(args).await {
if e.to_string().contains("missing required field `op`") {
return Ok(ToolResult::error("todo requires 'op' (add|edit|update_status|remove|replace|clear|list)"));
}
return Err(e);
} Prevention
- Always dispatch with an explicit op — never rely on a default
- Validate op against the closed set before invoking
- Keep the unknown-op soft error's list and the schema in sync
When it happens
Trigger: Model omits op entirely; op passed as null or a non-string; wrapper calling the tool with only per-op fields (content, id) and no op.
Common situations: Models assuming a default op; argument builders keyed by operation without embedding the op field.
Related errors
- missing `cards` for op=replace
- missing required field `{key}`
- invalid approvalMode '{other}' (expected required|not_requir
- invalid approvalMode type (expected required|not_required|nu
- Missing 'agent' parameter
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/614ebc372bf7b620.
Report an issue: GitHub.