---
title: Native cron
description: Recurring child issues, shared job definitions, and attributed run evidence.
last_edited: 2026-10-12
---

Kata creates recurring child issues and inbox requests from enabled native
notification jobs. The owning hub daemon handles delivery; consumers read the
ordinary inbox and decide what to do.

Kata also stores portable job and workflow definitions and independent run
observations. External adapters schedule other jobs, resolve checkout and provider
configuration, create or update ordinary issues, deliver notifications, and launch processes.
An observation records evidence; its status does not control whether anything runs.

## Vocabulary

| Name | Meaning |
| --- | --- |
| Job | Configured work: its trigger, action, and execution settings. |
| Workflow | Reusable command or prompt steps and their dependencies. |
| Run | One execution and its reported status and results. |

An execution job can invoke a workflow or supply a single prompt. Each execution has its
own run UID. A workflow can also be referenced directly by a run.

Issue responsibility uses Kata's existing `claim`, `assign`, `unassign`, and
owner vocabulary. A federation lease provides exclusive coordination on an
issue. Recording a run changes neither ownership nor leases. See
[federation leases](../operations/federation.md#leases-and-write-gates) for that mechanism.

## Definitions

`kata cron job` and `kata cron workflow` support `create`, `list`, `show`,
`update`, `delete`, and `restore`. Commands return JSON. Creation retains a ULID:

```sh
kata cron job create --uid <job-uid> --file job.json --json
kata cron job list --json
kata cron workflow create --uid <workflow-uid> --file workflow.json --json
kata cron workflow list --json
kata cron job update <job-uid> --expected-event-uid <event-uid> --file replacement.json --json
kata cron job delete <job-uid> --expected-event-uid <event-uid> --json
```

Creation requires no expected event UID. Updates, deletion, and restoration
require the current `expected_event_uid`, either as the flag or in the request
body; when both are present they must match. Conflicts require inspecting the
current definition. A lost create response can be inspected using the retained
UID. A repeated create does not silently become an update. Tombstones remain
readable, and `list --include-deleted` includes them. Deleting a workflow is
refused while a live job in the project uses it; update or delete that job
first.

Enabling a native recurring issue job activates delivery by the owning daemon.
Other jobs require an adapter installation to schedule them. No job launches a
process inside Kata.
Secret references are names, and checkout keys are portable names. Credentials,
host paths, and runtime handles are not part of this shared contract. Opaque
option objects retain exact JSON numbers.

### Request files

The `--file` body for `create` and `update` is a JSON object with a `name` and
a `definition`. Kata trims `name`, which must then be 1–256 bytes. Optional `actor` must
match `--as` when both are given; optional `expected_event_uid` applies to
`update` only. A job request file looks like this:

```json
{
  "name": "Weekday review",
  "definition": {
    "version": 1,
    "kind": "job",
    "enabled": true,
    "trigger": {"kind": "cron", "cron": "0 9 * * 1-5", "timezone": "America/New_York"},
    "action": {"kind": "execute", "prompt": "Review open pull requests and summarize risks."},
    "issue": {"kind": "per-run", "title": "Weekday review", "deadline_offset_seconds": 28800},
    "overlap": "forbid",
    "catchup": "skip",
    "checkout_key": "main-repo",
    "timeout_seconds": 3600,
    "secret_refs": {"GITHUB_TOKEN": "github-review"}
  }
}
```

Definitions are strictly decoded: unknown fields are rejected, and each
definition is bounded to 256 KiB. A *portable name* is 1–256 bytes of letters,
digits, `-`, `_`, and `.`, and is neither `.` nor `..`.

### Job definitions

| Field | Rule |
| --- | --- |
| `version` | Required; must be `1`. |
| `kind` | Required; must be `"job"`. |
| `enabled` | Boolean; defaults to `false`. |
| `trigger` | Required; see [triggers](#triggers). |
| `action` | Required; see [actions](#actions). |
| `issue` | Issue policy; see [issue policies](#issue-policies). Required for `execute` and native recurring issues. |
| `overlap` | Required; `forbid` or `allow`. |
| `catchup` | Required; `skip`, `latest`, or `all`. |
| `checkout_key` | Optional portable name. Not allowed with `notify`. |
| `grace_seconds`, `timeout_seconds` | Optional; zero or greater. |
| `secret_refs` | Optional object mapping portable names to portable names. |
| `options` | Optional JSON object for the adapter. |

`options` objects, at any depth, cannot contain these keys (compared without
case): `token`, `api_token`, `api_key`, `password`, `secret`, `credentials`,
`authorization`, `cwd`, `path`, `checkout_path`, and `local_path`.

### Triggers

Every trigger has a `kind` and accepts an optional IANA `timezone`, such as
`America/New_York`. A trigger cannot carry fields that belong to another kind.

| `kind` | Required fields |
| --- | --- |
| `manual` | None. |
| `cron` | `cron`: a five-field expression (minute, hour, day of month, month, day of week) or a descriptor such as `@daily`. |
| `interval` | `interval_seconds`: a positive integer. |
| `once` | `at`: an RFC 3339 instant. |
| `issue-scheduled` | `issue_uid`: the ULID of the issue whose `scheduled_on` date fires the job. |
| `issue-deadline` | `issue_uid`: the ULID of the issue whose `deadline_on` date fires the job. Optional `lead_seconds` (zero or greater) is how long before the deadline to fire. |

### Actions

| `kind` | Fields and rules |
| --- | --- |
| `execute` | Exactly one of `prompt` (not blank) or `workflow_uid` (a workflow ULID). Requires an `issue` policy. Cannot use an `issue-deadline` trigger. Cannot carry `recipient` or `message`. |
| `notify` | `recipient`: an exact inbox address of at most 128 bytes, such as `worker` or `worker/adapter`, or `current-owner-or-author`. `message`: at most 1024 bytes; required unless `recipient` is `current-owner-or-author` or this is a native recurring issue job. Cannot carry `prompt` or `workflow_uid`, and the job cannot set `checkout_key`. |

A `notify` job with an `issue-scheduled` or `issue-deadline` trigger targets
that issue and may omit `issue`. Any other `notify` job requires an `issue`
policy of kind `existing` or the native recurring `per-run` policy below.

### Issue policies

| `kind` | Fields and rules |
| --- | --- |
| `existing` | `uid`: the ULID of an existing issue. No other fields. |
| `per-run` | `title` (not blank) and optional `body`. Optional `scheduled_offset_seconds` and `deadline_offset_seconds` are relative date offsets, in seconds, for the issue the adapter creates. No `uid`. |

For `notify`, `per-run` instead requires `parent_uid` and a nonblank `title`.
It accepts an optional `body`, but no `uid` or date offsets. This selects native
recurring child delivery. `parent_uid` is not accepted on execution jobs.

### Recurring child issues

Keep an ordinary parent issue open for the continuing commitment, then create a
job with `--parent <ref>`. For example, save this request as `job.json`:

```json
{
  "name": "Monthly service review",
  "definition": {
    "version": 1,
    "kind": "job",
    "enabled": true,
    "trigger": {"kind": "cron", "cron": "0 9 1 * *", "timezone": "UTC"},
    "action": {"kind": "notify", "recipient": "current-owner-or-author"},
    "issue": {
      "kind": "per-run",
      "title": "Review service",
      "body": "Check health and record any follow-up work."
    },
    "overlap": "allow",
    "catchup": "all"
  }
}
```

Create the job with the parent's bare short ID, qualified ref, or full UID:

```sh
kata cron job create --parent example-project#abc4 --file job.json --json
```

The CLI resolves the ref and stores its full UID in `definition.issue.parent_uid`.
The stored definition stays UID-only, so short ID growth and project renaming do
not change its parent identity. API clients can still supply `parent_uid` in the
request. If the file supplies it alongside `--parent`, both must name the same
UID. `--parent` requires `issue.kind: "per-run"`; execution jobs cannot carry a
parent binding.

The job uses the parent's project when no project is otherwise selected. An
explicit `--project` or workspace binding must match that project. Job updates
also accept `--parent`, together with the current `--expected-event-uid`; a
native job's existing parent binding remains immutable.

Each due occurrence creates one ordinary child with a parent link and an inbox
request. Earlier children may remain unfinished. The child copies the parent's
current owner at delivery; an unowned child remains unowned. Routing follows that
owner, or the parent author when unowned. Set an exact recipient such as
`worker/consumer` to keep routing fixed. An omitted message uses Kata's default
recurring notification message. Read with `kata inbox --for worker/consumer`,
then clear the request with `kata notify <child-ref> --to worker/consumer --clear`
after handling it. Reading does not acknowledge it.

The title gains a UTC occurrence timestamp. The configured title can be at
most 991 characters, so the child title stays within 1024 characters. Children carry
`cron.occurrence` provenance and no `scheduled_on`, so the ordinary date sweeper
does not request the same occurrence again. Clearing a request, closing a child,
or restarting the daemon never rewinds delivery. Native delivery creates no run
observation; a consumer may record its own execution evidence separately.

Native jobs accept only `cron` and `interval` triggers and require
`overlap: "allow"`. Calendar expressions use the trigger's IANA timezone, with
UTC as the default. They follow that timezone's daylight-saving transitions.
An embedded `TZ=` or `CRON_TZ=` must agree with `timezone`. Use
`{"kind":"interval","interval_seconds":7200}` for an elapsed two-hour cadence; intervals must be at least 60 seconds;
`@every` is not supported for native delivery. Intervals start at the first
binding time plus one interval. Calendar schedules start strictly after that
binding time, without historical backfill.

| `catchup` | After downtime or a pause |
| --- | --- |
| `all` | Create every missed child, at most 1000 per job per sweep. Later sweeps continue from durable progress. |
| `latest` | Create only the newest missed occurrence. Calendar scans examine at most 20,000 occurrences per sweep, retain a fixed scan horizon, and deliver when that scan finishes. Intervals find the newest occurrence directly. |
| `skip` | Require positive `grace_seconds`; discard older occurrences and deliver those within the grace window, with the same bounded sweep limits. |

The existing daemon sweep runs at startup and every 30 seconds by default. Keep the authoritative hub running
for unattended delivery. Local projects deliver locally; federated projects
deliver only on their enabled owning hub. Spokes never deliver, even with push
writes enabled, and cannot create or edit native recurring definitions. An old
peer that lacks `cron_notify_children_v1` cannot receive a project containing
native recurring state. Capability refusal happens before a baseline or cursor
change. Projects with native recurring delivery history cannot be adopted into
a different hub through spoke snapshots; the refusal preserves their existing
local authority and history.

Create, update, delete, and restore retain ordinary authorization and expected
revision checks. Once bound, a job's parent cannot change and the job cannot
become an adapter job. Edits preserve committed progress and the original
interval anchor; an unfinished latest scan restarts against the new definition.
Disable the job or close its parent to pause delivery. An archived project or a
deleted parent also pauses it. Re-enabling, reopening, or restoring resumes the
configured catch-up policy. A pause does not skip occurrences: with
`catchup: "all"`, resuming creates every child missed while paused. This
includes the time since creation for a job created disabled. Use `latest` or
`skip` when a pause must not backfill. Deleting the job stops delivery until
restoration.
Keep the parent in the job's project. Kata rejects moving the parent alone
while its native job exists, including when the job is disabled or deleted;
restoring the job can resume its binding. Merge projects to move the job and
parent together.

Child creation, its initial notification, and the parent's
`cron.progress.<job-uid>` checkpoint commit together. Each delivery therefore
changes the parent's revision and update time; re-read the parent before a
conditional edit. Backup, restore, federation,
and project merge preserve this metadata. Delivery fails closed if progress is
missing or malformed; ordinary issue writes and spoke ingest cannot change or
delete progress or occurrence provenance. Recurrence templates also reject these
system keys, including when older templates are materialized. Inspect that state before recovery;
there is no implicit reset that could recreate already delivered children.
Native jobs reject execution timeouts, secrets, adapter options, checkout keys,
and issue date offsets.

### Workflow definitions

A workflow definition has `version` `1` and a non-empty `steps` array. It may
also carry `about`, `input`, and `options` (with the same key rules as job
options). Each step has these fields:

| Field | Rule |
| --- | --- |
| `key` | Required portable name, unique within the workflow. |
| `kind` | `command` (requires `command`, no `prompt`) or `prompt` (requires `prompt`, no `command`). |
| `after` | Optional keys of earlier steps this step depends on. |
| `retries` | Optional; 0–100. |
| `options` | Optional JSON object for the adapter. |

```json
{
  "name": "Review then test",
  "definition": {
    "version": 1,
    "about": "Review a change, then run the tests.",
    "steps": [
      {"key": "review", "kind": "prompt", "prompt": "Review the change."},
      {"key": "test", "kind": "command", "command": "make test", "after": ["review"], "retries": 1}
    ]
  }
}
```

## Capabilities

`kata cron capabilities --json` reads the project's UID and advertised
`event_features` without changing state. The corresponding GET is
`/api/v1/projects/{project_id}/cron/capabilities`; the
`X-Kata-Event-Features` header advertises `cron_v1` and
`cron_notify_children_v1`. Missing, unknown, or
failed advertisements remain distinct outcomes. This is an ordinary project
read and creates no permissions. Ordinary authentication, project isolation,
and issue-scoped route restrictions apply.

## Independent run evidence

An adapter creates one independent run UID and retains it across retries:

```sh
kata cron run observe <run-uid> --json-input observation.json --json
kata cron run list --limit 100 --json
kata cron run show <run-uid> --json
```

The HTTP operation is `PUT /api/v1/projects/{project_id}/cron/runs/{run_uid}`
(`observeCronRun`). An omitted JSON `teammate` inherits `--teammate` or
`KATA_TEAMMATE`; an explicit empty flag suppresses that inheritance. An explicit
JSON string or `null` freezes attribution across retries and ignores the
environment. An explicit flag must match the frozen value (`null` matches an
empty flag). An empty JSON string remains invalid; it is never replaced by the
environment. For example:

```json
{
  "job_uid": "01ARZ3NDEKTSV4RRFFQ69G5FAA",
  "definition_event_uid": "01ARZ3NDEKTSV4RRFFQ69G5FAB",
  "occurrence_key": "daily:2026-10-06",
  "actor": "worker",
  "teammate": "adapter",
  "executor_label": "local-worker",
  "status": "running",
  "summary": {"version": 1, "message": "Review started"},
  "expected_revision": 0
}
```

At least one paired job/event or workflow/event reference is required; both pairs
may be supplied. References identify historical definitions, including earlier
versions and tombstones, within the project. An optional issue UID is an
ordinary existing issue in that project. Distinct run UIDs may share the same
occurrence key and issue. No occurrence is reserved by logging a run.

UID, project, definition references, occurrence key, issue UID, attributed actor,
teammate, executor label, and server-created time are immutable. Status, summary,
start time, and end time are mutable evidence. Supported statuses are `running`,
`succeeded`, `failed`, `cancelled`, and `unknown`; they impose no transition or
launch protocol. Summary version 1 permits a message and nonnegative integer
input/output token counts and is bounded to 64 KiB. The whole observation and
its event are bounded to 96 KiB. Unknown fields are rejected. Times are RFC3339
instants; an end cannot precede a start. An occurrence key is at most 1024
characters; actor and optional executor label are at most 256 bytes. Teammates
use Kata's existing 1–64 character ASCII names.

A new observation uses expected revision 0. A changed observation requires its
current revision. An identical retry of the same UID and canonical evidence
returns `replayed: true`, emits no event, and changes no revision even when its
expected revision is stale. A different immutable identity always conflicts.
Authenticated actor attribution overrides an untrusted request actor, and
revocation is rechecked in the write transaction. Logging does not change an
issue's owner, dates, lease, status, or readiness.

Responses contain `run`, the exact committed `events`, and `replayed`. Run
listing is project-scoped, accepts `--job-uid` to list one job's runs, and
orders by creation time and UID descending. Use `next_before_uid` with `--before-uid` for another
page. Ordinary federation publishes observations from spokes, folds evidence
by original event provenance per run UID, and preserves independent runs in
baselines and backup. Run identity conflicts fail rather than replacing a run.

## Native issue planning dates

`kata show <issue-ref> --planning-dates --json` and GET
`/api/v1/projects/{project_id}/issues/{ref}/planning-dates` read one consistent
issue revision with required nullable `scheduled_on` and `deadline_on` objects.
Each present object contains `field`, raw `value`, resolved `timezone`, and a
UTC `instant`. The flat response also contains `project_id`, `issue_uid`, and
ordinary `revision`.

Native date parsing applies the issue timezone first, then the recurrence
fallback for scheduled dates, then daemon default timezone and UTC. Deadline
dates have no recurrence fallback. Explicit UTC values resolve to UTC. The read
uses native civil-time and DST behavior; it evaluates no clock and changes no
readiness or persisted state. Normal project and issue-subtree read policy applies.

## Adapter responsibilities

Adapters must exclude `notify` jobs with `issue.kind: "per-run"` from their
schedulers; those jobs belong to the native hub. Existing strict decoders reject
the new `parent_uid` shape instead of scheduling it.

Shared `enabled` configuration does not activate an adapter installation. Each
adapter owns its local activation, scheduler, overlap and catchup handling,
checkout and secret mappings, process lifecycle, and conversation recovery.
A second adapter may independently execute the same occurrence. Each execution
keeps a separate run UID; no shared observation reserves the issue or occurrence.
Local transcripts, process IDs, session handles, credentials, and delivery
buffers are not native run evidence. An adapter may buffer a failed observation
write and retry it later without blocking its own work.

The adapter must refresh current definitions before starting new scheduled work;
offline cached data is for labeled reads. Portable `cron` triggers use the
configured trigger timezone. Issue-date triggers use the planning-date instant
above: issue timezone, recurrence fallback, daemon default and UTC remain
authoritative. Future `scheduled_on` and `someday` defer ordinary execute
readiness; `deadline_on` never does. Deadline actions are notification-only.

Kata's default-date sweeper continues to request the current owner's attention,
or the author's when unowned, for reached native schedules and deadlines. The
same-source, zero-lead `current-owner-or-author` alias must not produce a second
adapter notification. Custom offsets and recipients use ordinary notifications.
Inbox addresses are exact: `worker` does not aggregate `worker/adapter`. Reads do
not clear requests; the recipient handler clears only after handling and reads
back. Attention is a replaceable per-issue/recipient slot, so replacement and clear
can race.

Shared definitions and run evidence inherit ordinary authentication, project/
issue-scoped roles, actor attribution, token revocation and credential-origin
checks. Capability discovery advertises compatibility only. Shared federation and
backup do not transfer local activation, checkout credentials, execution buffers
or conversation handles; preserve those separately under the adapter's backup
workflow. PostgreSQL runtime roles still cannot perform owner-only migrations.

[Herdr Kata](https://github.com/salmonumbrella/herdr-kata/blob/main/docs/native.md)
is one adapter that implements this contract; its documentation covers setup.

## Storage and backup

SQLite and PostgreSQL schema 31 store native cron data in `cron_jobs`,
`cron_workflows`, and `cron_runs`. JSONL export and import retain shared
definitions, including tombstones, and bounded run evidence. Native recurring
delivery reuses ordinary issue metadata and parent links; it adds no tables or
schema version. Keep parent checkpoints and child provenance in the same backup. Runs with
`running` or `unknown` status do not prevent an explicitly requested restore,
federation reset, adoption, or project merge; those operations keep their
ordinary identity, revision and credential guards. See
[PostgreSQL migrations](../development/postgres-migrations.md) for owner-only
migration, backup, runtime grants, and rollback requirements.

## Routes, events, and records

Workflow routes are `/api/v1/projects/{project_id}/cron/workflows` and
`/api/v1/projects/{project_id}/cron/workflows/{cron_uid}`. Responses use
`workflow` or `workflows`; job actions and run observations use `workflow_uid`,
and run observations pair it with `workflow_definition_event_uid`. Events use
`cron.workflow.*`, and JSONL workflow records use `cron_workflow`.
