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.
| Helper | Feeds | What for |
|---|---|---|
emit_event '<json>' | Timeline tab | one event per milestone of the execution, not in a tight loop |
append_log '<text>' | Logs tab | log lines |
log_http_call '<text>' | Logs tab | every call to an external system's API, with secrets masked |
create_todo '<json>' | Pending tab | a 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.
| Key | Convention |
|---|---|
type | step.started and step.completed per step |
step | step number as a string ("1", "2") |
level | thinking, info, success, warning or error |
message | what is happening, in one sentence, in the execution's language |
payload | structured 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:
| Situation | What to do |
|---|---|
| Ambiguous data | pick the most likely reading, say which one you picked and create a pending item with the doubt |
| An item failed | create a pending item with the real error and move on to the next ones |
| Volume above expected | process up to the limit and create a pending item listing the excess |
| An absolute prerequisite is missing | write 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​
| Variable | What it brings |
|---|---|
$PARAMETERS | the run's parameters, in JSON, already validated against parameters_schema |
$REFERENCE_DATE | the reference date (YYYY-MM-DD): the execution's "today" |
$LOCALE | the language of whoever triggered the run; write events and result in it |
$INPUT_FILES | one path per line, only with accepts_input_files: true. The files are already on disk. |
$OUTPUT_FILES_DIR | only with generates_output_files: true: everything saved here is published |
| secrets | each 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
resultis 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".summaryis 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:
| Rule | Why |
|---|---|
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 changes | A 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 idempotent | The 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 state | Each 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 intermediates | Session, 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:
| Tab | Who feeds it |
|---|---|
| Timeline | the 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) |
| Logs | the helper records one line per tool or batch |
| Pending | the helper creates its own |
| Result | only 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"}')"
```