Skip to main content

The task body

The body of the SKILL.md is the instructions the agent follows. In an execution, it runs inside the /kadu:agent-run-task wrapper, which has already prepared everything: instrumentation helpers, reference date, HTTP call logging and finalization. Do not redo any of that.

The helpers​

The helpers live in /tmp/agent_state.sh. Each shell block in the body starts with source /tmp/agent_state.sh.

HelperFeedsWhat for
emit_event '<json>'Timeline tabone event per milestone of the execution, not in a tight loop
append_log '<text>'Logs tablog lines
log_http_call '<text>'Logs tabevery call to an external system's API, with secrets masked
create_todo '<json>'Pending taba pending item that outlives the execution
mark_body_started—mandatory sentinel (below)

:::danger Do not redefine the helpers Redefining any of them in the skill body takes the sending off the path that masks secrets. Use the ones the wrapper wrote. :::

The sentinel​

At the end of the execution, the wrapper checks whether the body marked that it started. Without the mark, it records execution.body_skipped and fails the execution (exit 2). So the body starts like this:

source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Fetching yesterday'"'"'s payments"}')"
mark_body_started

Events: type and message are required​

Every event needs type and message; level, step and payload are optional. An event without message is rejected (400) and the sender ignores the error: the event disappears from the timeline, with nothing in the log. If a step went missing from the timeline, suspect this first.

KeyConvention
typestep.started and step.completed per step
stepstep number as a string ("1", "2")
levelthinking, info, success, warning or error
messagewhat is happening, in one sentence, in the execution's language
payloadstructured data for the step (counts, ids)

Logs and HTTP calls​

log_http_call is for calls to integration APIs (Fintera, Pagar.me, Linear, Slack), one entry per call:

source /tmp/agent_state.sh
RESP=$(curl -sS -w '\n%{http_code}' -u "$FINTERA_TOKEN:X" \
"https://financeiro.fintera.com.br/entities/$FINTERA_ENTITY_ID/receivable_accounts?per_page=50")
log_http_call "$(printf 'GET /receivable_accounts?per_page=50\n→ %s' "$(tail -n1 <<<"$RESP")")"

A high-volume tool (scanner, crawler, batch load) makes hundreds or thousands of requests: logging each one drowns the Logs tab and hides the calls that matter. Summarize in one append_log line (nuclei: 1,240 requests → 3 findings) and reserve log_http_call for the APIs.

There is no user in the execution​

The execution is automatic. Every question is a silent hang: the task stays stuck until it hits the 60-minute limit. Where an interactive skill would ask, the task decides and records:

SituationWhat to do
Ambiguous datapick the most likely reading, say which one you picked and create a pending item with the doubt
An item failedcreate a pending item with the real error and move on to the next ones
Volume above expectedprocess up to the limit and create a pending item listing the excess
An absolute prerequisite is missingwrite the result saying where it stopped and exit with a non-zero code
source /tmp/agent_state.sh
create_todo "$(jq -nc --arg doc "$CNPJ" \
'{title:"Payment with no matching receivable",description:"No open account for the CNPJ in the amount paid.",payload:{cnpj:$doc,valor:1520.00}}')"

Write the skill for both modes when it is also useful in Claude Code, with a person on the other side:

if [ -n "${GITHUB_ACTIONS:-}" ]; then MODE=task; else MODE=local; fi

Inputs​

VariableWhat it brings
$PARAMETERSthe run's parameters, in JSON, already validated against parameters_schema
$REFERENCE_DATEthe reference date (YYYY-MM-DD): the execution's "today"
$LOCALEthe language of whoever triggered the run; write events and result in it
$INPUT_FILESone path per line, only with accepts_input_files: true. The files are already on disk.
$OUTPUT_FILES_DIRonly with generates_output_files: true: everything saved here is published
secretseach key in required_secrets arrives as an environment variable ($FINTERA_TOKEN)
source /tmp/agent_state.sh
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
REPO_URL=$(jq -r '.repo_url // empty' <<<"$PARAMS")

Do not invent a missing value. An empty essential parameter is an honest failure, not a reason to guess.

Finalization: you write the file, the wrapper sends it​

At the end, write ./.execution-result.json with result and summary:

source /tmp/agent_state.sh
RESULT=$(cat <<'MD'
## Summary

12 payments settled, 2 skipped (see pending items).
MD
)
SUMMARY='{"processados":12,"pulados":2}'
jq -n --arg result "$RESULT" --argjson summary "$SUMMARY" \
'{result:$result, summary:$summary}' > ./.execution-result.json
  • result is not a status. It is the final Markdown that the Result tab shows, in the language of $LOCALE. {"result":"success"} makes the tab display the word "success".
  • summary is an object of counters, not a sentence.
  • The wrapper is what sends the result to the console, from this file. Do not call the finalization in the body and never resend the logs at the end: the Logs tab is fed gradually, and a resend overwrites it.

Fatal error midway: write the .execution-result.json anyway, saying where it stopped, and exit with a non-zero code. The failure status comes from the exit code; do not try to mark the failure on your own.

Conversational tasks​

With conversational: true, the person can request changes after the execution finishes (the Conversation card in the console, or POST /v1/executions/{id}/continue). Each request creates a child execution, and the child turn receives:

  • the same parameters, reference date and environment as the previous one. The request arrives in $ADDITIONAL_INSTRUCTIONS;
  • the previous one's agent session: the model sees what it read, ran and produced;
  • the previous one's files, at the same paths: .execution-files/input/, .execution-files/output/ ($OUTPUT_FILES_DIR) and .execution-files/state/. The rest of the working directory is discarded between turns.

The wrapper and the body run again, from the start: the model decides, from the request and the history, what to redo. This changes how the body is written:

RuleWhy
Keep in .execution-files/state/ whatever is worth reusing (script, assets, editing project)It is the only working directory preserved, besides inputs and outputs.
Write outputs to $OUTPUT_FILES_DIR as usual, overwriting the file that changesA restored output that comes back unchanged is not published again; the changed one is published as the new turn's output.
Make every effect on an external system idempotentThe body runs again on every turn. Without checking before creating, a turn that only adjusts a text duplicates the first turn's charge, account or email.
Write each turn's result as the complete current stateEach execution's Result tab shows only its own result.
Do not depend on anything outside .execution-files/Installed packages, /tmp and the rest of the working directory do not survive.
Avoid large intermediatesSession, inputs, outputs and state add up to at most 1 GiB. Above that the execution finishes normally, but cannot be continued.

Both executors continue conversations: claude_code resumes its transcript, goose its session (goose run --resume). The session belongs to the executor that saved it: if the task's tier or executor override changes between turns, the next one is refused (409 EXECUTOR_CHANGED) and the task has to be run again. On goose, the cheaper models of tiers 1 and 2 get a history that grows every turn: check that the result holds up after a few turns.

A helper that runs inside a task​

A small helper (sending a Slack message) does not need its own instrumentation: the task that calls it records the step. But when the task orchestrates helpers that do the bulk of the work, silent helpers leave the timeline and logs empty for almost the whole execution.

A helper called inside an execution runs in the same process and sees the helpers. So it instruments its own steps:

TabWho feeds it
Timelinethe task opens and closes each phase; the helper emits its own steps' step.started/step.completed, with counts (the individual item goes to a file, not to an event)
Logsthe helper records one line per tool or batch
Pendingthe helper creates its own
Resultonly the task writes .execution-result.json; the helper returns a file and a summary

Document this in a "Kadu instrumentation" section of the helper, so whoever reads it knows which tab to look in.

Body template​

# Olá, Kadu

Example task: it accesses no external system and asks no questions.

## Step 1 — start

```bash
source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Preparing the greeting"}')"
mark_body_started
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
NOME=$(jq -r '.nome // "time"' <<<"$PARAMS")
append_log "Parameters received: $PARAMS"
```

## Step 2 — result

Write a short greeting for `$NOME`, in the language of `$LOCALE`, mentioning
the reference date (`$REFERENCE_DATE`). Write the result:

```bash
source /tmp/agent_state.sh
RESULT="## Hello, $NOME!"
jq -n --arg result "$RESULT" '{result:$result, summary:{cumprimentos:1}}' > ./.execution-result.json
emit_event "$(jq -nc '{type:"step.completed",level:"info",step:"2",message:"Greeting written"}')"
```