Task Definition and Action Contracts
task.md schema, when trigger semantics, and action contracts
Task Definition and Action Contracts
Task definition file
.downcity/task/<taskId>/task.mdtaskId is derived from normalized title, and title should stay unique.
Frontmatter fields
| Field | Required | Description |
|---|---|---|
title | Yes | Unique task name |
description | Yes | Goal description |
sessionId | Yes | Session id that receives the final task result |
when | Yes | Trigger: @manual / cron / time:ISO8601 |
status | Yes | enabled / paused / disabled |
kind | No | agent (default) or script |
review | No | Only used by kind=agent; enables user-simulator multi-round review when true, otherwise single-round |
when semantics
@manual: manual trigger only.- cron expression: periodic scheduling.
time:2026-03-08T10:30:00+08:00: one-shot trigger (timezone required).
Cron expressions are interpreted in the local timezone of the runtime process. If the runtime runs in Asia/Shanghai, 0 13 * * * means 13
After one-shot execution succeeds:
status -> pausedwhen -> @manual
Actions
| Action | Purpose | Key response |
|---|---|---|
list | list task definitions | tasks[] |
create | create task definition | may return reusedExisting=true |
update | update definition | writes task.md |
run | manual run once | returns accepted=true + executionId immediately |
status | explicit status set | enabled/paused/disabled |
enable/disable | status shortcuts | same as status update |
delete | remove definition and run history | returns task dir path |
Create de-dup logic
create now deduplicates by exact title only:
- same
titleexists: reuse existing task (reusedExisting=true) - otherwise: create a new task
No semantic-similarity dedup is applied.
run contract
run is async-accepted:
- verify task exists
- start
runTaskNow(...)in background - return immediately with
accepted=true,message, andexecutionId
The message explicitly tells the caller:
- the task has started
- the task result will be sent to the user automatically when it finishes
- the agent can continue the next steps immediately
- there is no need to wait for the task to finish
Automatic delivery after completion:
- Downcity uses chat service
sendto deliver the task's final result body back to the chat bound tosessionId. - Because of that, task bodies usually should not ask the executor to call
city chat sendagain. - Only add explicit send steps for cross-session delivery, extra notifications, or multi-target fanout.
Caller behavior rules:
- do not manually forward the task output again after
accepted=true - do not proactively read the run directory,
run-progress.json, oroutput.mdto poll - only inspect run artifacts when the user explicitly asks for debugging or execution details
Current agent rules:
- single-round by default
- multi-round review only when
review=true - valid output text is enough by default; successful
chat_senddelivery is no longer required
How to write task bodies
Treat body as an execution spec, not a vague reminder.
Recommended agent task structure
# Goal
- State the final deliverable clearly.
# Context and Inputs
- Add sources, scope, constraints, assumptions, and references.
# Steps
1. Understand the completion criteria.
2. Gather information or execute the work.
3. Save key intermediate artifacts into the run directory.
4. Produce the final result body.
# Output Requirements
- Return the final result directly.
- Specify whether it should be Markdown, JSON, a table, etc.
- Avoid long raw logs.
- Do not call `city chat send` again unless extra delivery is required.
# Trigger and Status Guidance
- draft/manual flow: `@manual` + `paused`
- stable recurring flow: cron + `enabled`
- one-shot execution: `time:<timezone-aware ISO datetime>`
# Notes
- Only send extra messages when cross-session or additional notification is truly required.Recommended script task structure
# Goal: one-line summary
# Inputs: env vars, files, command dependencies
# Failure conditions: when the script should exit 1
set -euo pipefail
# 1. Prepare inputs
# 2. Run main logic
# 3. Print final result (this output is what gets sent back to the user)Default guidance
- If the user did not explicitly ask for automatic triggering, prefer
@manual+paused. - Task bodies should describe what the final result should look like, not just say "go do it".
- Write key intermediate artifacts into the run directory when possible.
CLI and API
CLI:
city task run daily-check --reason "manual"API:
city service command task run --payload '{"title":"daily-check","reason":"manual"}'city task ... now uses local IPC by default, so it does not need a Bearer token for normal local usage. Only explicit --host or --port switches it into remote HTTP mode; in that case, authentication identity stays separate from sessionId:
- the Bearer Token answers "who is calling"
sessionIdanswers "which execution context this task belongs to"