tinyhumansai/openhuman · warning
invalid `cards`: {e}
Error message
invalid `cards`: {e} What it means
op=replace found `cards` but serde_json could not deserialize it into Vec<TaskBoardCard> (todo.rs:158). The {e} chain names the exact mismatch: cards is not a JSON array, or an element is missing a required card field or has a wrong type. Distinct from the missing-cards error, which fires only when the key is absent.
Source
Thrown at src/openhuman/agent/tools/todo.rs:158
patch.content = optional_string(&args, "content");
ops::edit(&location, &id, patch).await
}
"update_status" => {
let id = required_string(&args, "id")?;
let status = required_string(&args, "status")?;
let status = ops::parse_status(&status).map_err(anyhow::Error::msg)?;
ops::update_status(&location, &id, status).await
}
"remove" => {
let id = required_string(&args, "id")?;
ops::remove(&location, &id).await
}
"replace" => {
let cards = args
.get("cards")
.ok_or_else(|| anyhow::anyhow!("missing `cards` for op=replace"))?;
let cards: Vec<TaskBoardCard> = serde_json::from_value(cards.clone())
.map_err(|e| anyhow::anyhow!("invalid `cards`: {e}"))?;
ops::replace(&location, cards).await
}
"clear" => ops::clear(&location).await,
"list" => ops::list(&location).await,
other => {
return Ok(ToolResult::error(format!(
"unknown op '{other}' (expected add|edit|update_status|remove|replace|clear|list)"
)))
}
};
match result {
Ok(snap) => {
let payload = json!({
"threadId": snap.thread_id,
"cards": snap.cards,
"markdown": snap.markdown,
});View on GitHub (pinned to a221052e0d)
Solutions
- Make cards a JSON array of card objects matching TaskBoardCard's required fields and types
- Read the {e} message — serde names the offending field and expected type precisely
- Round-trip validate with serde_json::from_value::<Vec<TaskBoardCard>> in wrappers before invoking the tool
Example fix
// before
{ "op": "replace", "cards": { "t1": { "content": "Ship" } } }
// after
{ "op": "replace", "cards": [{ "id": "t1", "content": "Ship the fix" }] } Defensive patterns
Strategy: validation
Validate before calling
// Rust — pre-flight the exact deserialization the tool will perform
let cards: Vec<TaskBoardCard> = serde_json::from_value(args["cards"].clone())
.context("cards payload does not match TaskBoardCard shape")?; Type guard
function isCardArray(v: unknown): v is Array<Record<string, unknown>> {
return Array.isArray(v) && v.every(c => typeof c === "object" && c !== null && "id" in c);
} Try / catch
// Surface serde's precise field error to the model for self-correction
if e.to_string().contains("invalid `cards`") {
return Ok(ToolResult::error(format!("cards shape invalid: {e:#}} — see card schema")));
} Prevention
- Send cards as a flat array of card objects with all required string fields
- Echo card objects from a prior list op rather than reconstructing from memory
- Round-trip validate with from_value before invoking the tool
When it happens
Trigger: cards passed as an object or map keyed by id instead of an array; card elements missing required fields (e.g. id/content) or carrying wrong JSON types (numbers for string fields); null cards (deserialization failure, not the missing-key path).
Common situations: Models echoing the board shape from memory instead of the schema; wrappers forwarding a UI state object whose card fields were renamed.
Related errors
- missing required field `op`
- missing `cards` for op=replace
- missing required field `{key}`
- invalid approvalMode '{other}' (expected required|not_requir
- invalid approvalMode type (expected required|not_required|nu
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/60929e4d5d509d14.
Report an issue: GitHub.