akm docs

Tasks

Task assets are strict, local automation sources. They live at <bundle>/tasks/<id>.yml and can be run directly or reconciled to cron, launchd, or Windows Task Scheduler with akm task sync. The task file is authored source; scheduler entries are derived OS state.

Task source v4 (version: 4) is the only task source grammar akm reads. A document with version: 3 or version: 2 fails to load with UsageError code TASK_SCHEMA_VERSION_UNSUPPORTED, naming akm migrate apply, which converts it. A declared version: 4 document whose schedule[] still carries a per-entry enabled key — 0.9.15's v4 grammar accepted it, this release's does not — fails the same way with TASK_SOURCE_INVALID (activation is host-local, below). Each such file fails on its own: akm task sync reports it and keeps reconciling every other task. Task source v4 adds typed inputs: and a single bounded output: schema (command targets only), and makes scheduling OPTIONAL rather than mandatory. akm task add authors task source v4 directly.

If you have version: 3 or version: 2 files on disk (from an earlier akm release), see Migrating to task source v4 below — akm migrate apply converts both generations in one pass and rewrites the file on disk (akm upgrade runs it after an install). The retired v3 grammar itself is documented at the bottom of this page (Task v3 (retired): grammar reference for migration) purely so you can read an old file while migrating it; it is no longer accepted as a standing grammar by any command in this release.

Files and schema

The only recognized task extension is .yml. A .yaml near miss is never indexed, scheduled, or run. Every task should declare version: 4; a document with no version: key, or a version: that is not a number, fails with TASK_SOURCE_INVALID (must be exactly 4. / is required and must be exactly 4.) — a genuinely malformed v4 document, not a legacy one. version: 3 and version: 2 fail with TASK_SCHEMA_VERSION_UNSUPPORTED naming akm migrate apply (see Migrating to task source v4). The published task schema describes the hand-authored contract; src/tasks/source/task-source-v4.ts is the authoritative bounded parser.

version: 4
name: Nightly review
uses: workflows/nightly-review
inputs:
  strict:
    type: boolean
    default: true
schedule: "0 4 * * *"
timeout: 30000

Task YAML is bounded before expansion: source size, YAML depth, aggregate node count, mapping width, string size, and collection sizes all have finite limits. Aliases, merge keys, custom tags, duplicate keys, accessors, and non-plain data are rejected rather than normalized.

Executable targets: uses or run

A task selects exactly one of uses or run; the two fields are mutually exclusive.

uses accepts these target shapes:

A GitHub Action locator (owner/repo[/path]@ref, e.g. actions/checkout@v4) is not a recognized uses: shape — it fails at parse with TASK_SOURCE_INVALID alongside every other unrecognized target. AKM never acquired or executed a remote action in any release; this is the removal of a recognized-but-rejected spelling, not a capability that used to work.

Agent refs such as agents/reviewer are personas and are not executable. Task refs such as tasks/nightly are also not executable. Local actions (./action) are rejected and Docker actions (docker://image) are unsupported. GitHub expressions are unsupported and rejected before dispatch.

The runtime applies this target-by-target field matrix. Validation is strict; fields are not silently discarded.

Target with / inputs task env Interpreter / execution
run No with; declare inputs: for typed parameters Allowed One authored string through the selected closed host shell
akm/command Required action object: exactly one of ref or content, plus optional portable arguments Allowed and passed through the command resolver Shared command authorization and lowering
commands/<name> No with; declare inputs: for typed parameters Allowed and passed through the command resolver Shared command authorization and lowering
workflows/<name> Declared inputs: become the child run's params A nonempty task env is rejected because the durable workflow runtime cannot consume it Fresh durable workflow start
scripts/<name>.<ext> No with; declare inputs: for typed parameters Allowed for the child process Closed extension-to-interpreter table below

Script refs use this closed table; any other extension fails before dispatch:

Extensions Interpreter
.sh sh
.ts, .js Bun; JavaScript and TypeScript script targets require Bun (the standalone binary uses its embedded Bun runtime)
.ps1 powershell -NoProfile -NonInteractive -File
.cmd, .bat cmd /d /s /c
.py python
.rb ruby
.go go run
.pl perl
.php php
.lua lua
.r rscript
.swift swift
.kt, .kts Kotlin (kotlin for .kt, kotlinc -script for .kts)

run is one non-empty shell string. It may specify shell from the closed host shell table bash, sh, zsh, pwsh, powershell, or cmd. Shell expansion is runtime behavior for an explicitly authored task run; AKM does not infer a shell from uses. working-directory must be a relative, contained path under the task's workspace root. Absolute paths, traversal, dangling links, and symlink escapes fail before execution.

Every field that used to live under v3's akm: options bag is a top-level key in task source v4: agent, engine, model, inference, tools, timeout, redact, maxSteps, and maxRetries, plus description, when_to_use, and tags. Environment entries (env) are literal string, number, or boolean values. Keep credentials out of task source; redact contains environment variable names, never secret values.

timeout: (milliseconds) means a different mechanism depending on the target. For run: (native shell/script) and workflows/<name> targets it is an outer supervisory deadline: the runner kills the child process, or aborts the workflow run at its next step boundary, when it fires. For uses: akm/command, commands/<name>, and any other agent/LLM dispatch target there is no outer process kill — timeout: instead resolves through the execution cascade (config/persona/command/task layers) into the dispatch's own deadline, and the SDK/CLI runner races each phase against it internally. Either way, a dispatch that times out is recorded as status: failed with detail.reason: "timeout" in task_history — not a silent completed — and akm task run exits non-zero for it; see health-advisories.md's task-fail-rate row for how akm health surfaces a timeout-dominant failure pattern.

Scheduling

Scheduling is optional. Omit schedule: entirely for a manual-only task: the source still parses, still runs with akm task run, and akm task sync silently contributes zero scheduler bindings for it (no OS entry, no failure) rather than rejecting the source for missing a trigger.

version: 4
name: Nightly review
run: akm improve --strategy default
schedule:
  - cron: "@daily"

A bare string (schedule: "0 8 * * 1") is shorthand for one trigger with no inputs. A list entry may set literal inputs; those literals are validated against the task's inputs: declarations both at parse time and again at akm task sync (once with declared defaults applied), and are delivered to the scheduled run: akm task sync compiles each entry's inputs into the scheduler binding's own invocation tail (akm task run <id> --scheduled --<name> <value>…, names sorted), so the fired run receives them exactly as akm task run <id> --<name> <value> would. Multiple schedule entries create deterministic scheduler bindings for the one source task.

Task source v4 has no enablement flag. A source describes what may run; it cannot authorize its own host scheduling. Activation is this host's list of fully-qualified refs in config.json under scheduler.enabled. A ref that is not listed is disabled. A config with no list at all (written before 0.9.17) means "keep what is installed": the first sync fills the list from the akm-written native bindings. Use akm task enable <bundle>//tasks/<id> and akm task disable <bundle>//tasks/<id> to change the list and immediately sync the affected bundle. akm task add enables its new task by default; --disabled writes the same task source but does not list it.

akm task run <id> executes a task immediately, including a disabled task. akm task sync scans every enabled configured bundle, reads only locally activated task/workflow refs, and reconciles the native scheduler one row at a time (see Operations). --bundle <name> narrows that pass to one active bundle. If every configured bundle is disabled, sync removes the attributable native entries without reading task content. Workflow targets create a fresh durable workflow freeze at fire time.

Typed inputs and output

version: 4
name: Review code
description: Summarize a pull request's changed surface
inputs:
  scope:
    type: string
    enum: [changed, all]
    default: changed
  strict:
    type: boolean
    default: true
  ticket:
    type: string
    required: true
output:
  type: object
  properties:
    summary: { type: string }
uses: commands/review
schedule:
  - cron: "0 8 * * 1"
    inputs: { scope: all, ticket: OPS-1234 }
timeout: 45000
engine: reviewer
redact: [TOKEN]

Input flags

akm task run <id> accepts one exact-name flag per declared inputs: entry, mirroring akm workflow run's parameter flags:

akm task run review --scope all --strict  # doclint:ignore

Flag names are task-specific — they come from that task's own inputs: declarations — so they are not listed on akm task run --help and the example above uses one task's actual declared names, not a fixed syntax. An undeclared flag fails with UNKNOWN_FLAG; a value that does not satisfy its declaration, or a missing required: true input supplied by neither a flag nor a default, fails with INPUT_BINDING_INVALID — both exit 2 with the usual {ok:false,error,code} envelope on stderr.

Where the materialized values go next depends on the task's own target, and is narrower than it may look: when the target is uses: workflows/<ref>, the values become the child run's params (the same with: → params path a workflow step's own composition uses); for every other target — run: shell, scripts/<ref>, commands/<ref> — the values are validated and then discarded. akm task run's own flags never populate an AKM_TASK_INPUTS environment variable or a ## Task inputs prompt block. Those two surfaces are a different delivery path: they exist only when a workflow step composes this task through uses: tasks/<ref> and a with: binding, resolved fresh for that step's own dispatch — see with: on a task-composed step. A scheduled run (schedule[].inputs, above) reaches the target through this same akm task run path, so it inherits the identical rule: delivered as params for a workflows/<ref> target, otherwise validated and discarded. akm task explain (below) shows the materialized values regardless of where they end up, which is the fastest way to check what a given akm task run invocation would actually deliver. akm task add --params renders --params values as typed inputs: declarations with default: values (typed from each JSON value's runtime type), not a with: bag.

akm task explain

akm task explain <ref> [input flags] prints a task's source path and version, its declared inputs: (name, type, enum, required, default), the values that would actually be supplied — with provenance (default | flag | schedule-binding) — the resolved target kind/ref, effective execution settings with field-level provenance, and schedule bindings. It is read-only: it never spawns anything, writes history, or touches the scheduler. A secret-shaped value (a declared default, a supplied value, or a schedule binding's literal) prints as "<redacted>" with its row marked redacted: true instead of the real value; an env: binding is shown as a name/ref only, never its resolved value.

akm task explain review --scope all  # doclint:ignore

akm task explain's default output (no --format flag) is the same raw JSON as --format json, byte for byte — there is nothing to lose by piping the default form into jq or a script. --format text is a separate renderer: it flattens the same envelope into dotted.path=value lines (the same convention akm config list --format text uses), not a copy of the JSON.

A secret-shaped input default (as opposed to a supplied value) is redacted the same way, but its accompanying explanation currently reuses workflow-parameter wording that does not quite fit a task input — treat the redaction itself as reliable even where the prose reads oddly. akm task run --<name> <secret-looking-value> never echoes the offered value in its error envelope, whether the value fails typed-flag coercion or fails the declared schema (an enum/minimum/maximum mismatch included) — both paths report only the violated constraint.

Migrating to task source v4

akm migrate status and akm migrate apply [--dry-run] run both migration generations against your task tree in one pass: task-v2 → task-v3 first, then task-v3 → task source v4 against the resulting files. Each generation keeps its own lock, backup, prevalidation, and rollback — a file blocked in the first generation does not stop the second generation from converting files that are already version: 3.

akm migrate apply --dry-run
akm migrate apply

The planner reports every input file as changed, skipped, or blocked, for both generations combined. Resolve every blocked file manually, then preview again. Apply validates a complete replacement before writing and backs up each original immediately before replacement.

Common v2 → v3 blocked reasons and what to do about each — these need a hand-authored replacement, not a re-run; see the 0.9.1 to 0.9.2 migration guide for the full v2 to v4 field mapping (command: array → run: + shell:, timeoutMs: → timeout:). Source-owned enabled fields are removed; native bindings that are provably enabled seed the host-local activation list:

Reason Meaning Fix
argv-array-has-no-portable-shell-string The task's command: is an argv array; no single shell string is provably equivalent. Rewrite the file by hand — a run: string plus shell: — using the field mapping above.
shell-quoting-changes-v2-whitespace-split-semantics, shell-operators-change-v2-literal-argv-semantics, shell-command-resolution-changes-v2-literal-argv-semantics The command: string contains quoting, shell operators, or a bare executable name whose v2 argv-exec behavior a v3 run: (host-shell) invocation cannot reproduce unambiguously. Review the command's intended shell semantics and author the v3/v4 run:/shell: fields by hand.

Common v3 → v4 blocked reasons and what to do about each:

Reason Meaning Fix
github-action-target-removed The task's uses: is a GitHub Action locator (owner/repo[/path]@ref); that spelling has no task source v4 equivalent. Rewrite the target as commands/, scripts/, workflows/, or akm/command by hand.
with-on-non-command-target A with: block is authored on a target other than uses: akm/command. Task-call inputs are declared and bound separately in v4 — author inputs: on the task and, if it is a workflow step's own composition, bind them with the step's with: instead.
ambiguous-scheduling-source The document declares both akm.schedule and on:. Pick one; the migrator will not guess which one wins.
read-only-source The owning source or file is not writable. Move or re-source the file somewhere writable, or edit it by hand.
invalid-v3-task The v3 document itself is structurally invalid (unknown fields, missing selector, malformed trigger, etc). Fix the underlying v3 document first — the migrator translates structure, it does not repair it.
generated-v4-validation-failed The converted bytes fail the real task source v4 parser; the detail carries the parse error. Read the detail — it names the offending field and why v4 refuses it — then fix that field in the v3 file and preview again.

The migrator translates structure, never intent: it never invents an inputs: declaration on a file's behalf, regardless of how inferable a with: value's shape looks — declaring inputs: (and rewriting a step to bind it) is left to the person editing the migrated file by hand.

A changed file can still carry an informational notice alongside its converted bytes, for a translation that is faithful but not one-to-one. Each notice is reported on that file's own plan entry, and it is the only record of a field the migrator resolved by dropping rather than rewriting — read them. Two cases produce one today:

The migrator is also installed as its own executable, akm-migrate, with the same status / apply [--dry-run] surface; akm migrate wraps it, and akm upgrade runs apply after installing a release. A tree that is already all version: 3 simply reports the first generation as current.

See the 0.9.1 to 0.9.2 migration guide for full before/after examples and recovery guidance.

Operations

Scheduler execution is at least once. Backends provide a stable invocation identity and AKM fences stale attempts, but an ambiguous process crash can be observed only after the external work has started. Make scheduled side effects idempotent where possible.

Task v3 (retired): grammar reference for migration

Nothing in this section is accepted by src/ in this release — it exists only so you can read an already-authored version: 3 file while deciding how to migrate it. The v3 grammar used an akm: options bag, an on: trigger block, and exactly one required scheduling source:

version: 3
name: Nightly review
uses: workflows/nightly-review
with:
  strict: true
akm:
  schedule: "0 4 * * *"
  enabled: true
  timeout: 30m

or, with the GitHub-shaped local trigger subset:

version: 3
uses: commands/review
on:
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch: {}

with: on any uses: target carried v3's untyped params bag (workflow refs consumed it as run params; command/script refs rejected it outright). A GitHub Action locator (owner/repo[/path]@ref) was a recognized uses: shape that was always rejected before dispatch — remote action acquisition was never implemented in any akm release. akm.enabled was source-owned scheduling state. akm migrate apply removes it; migration preserves actual host activation only when the native scheduler proves that the corresponding binding is enabled.

See Migrating to task source v4 above to convert a file out of this grammar.

See also