# Schedule recurring work

A cron sends a saved prompt to the projects you pick in a workspace, or to none, on a schedule. Each time it fires, Sensei hands the prompt, word for word, to the Cron Agent in a disposable sandbox that clones the selected repositories, if any. A run opens no chat session of its own. Every workspace cron run spends the workspace's Credentials, never a member's Subscription. To manage crons in the web app, see [Crons](https://kosuke.ai/docs/automate/crons.md). A Personal Cron has its own endpoints under `/api/user/crons`; see [Personal crons](https://kosuke.ai/docs/automate/crons.md#personal-crons).

## Create a cron

```bash
curl -s -X POST "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/crons" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly dependency check",
    "projectIds": ["'"$PROJECT_ID"'"],
    "prompt": "Check for outdated dependencies and update the patch versions.",
    "cronExpression": "0 6 * * 1"
  }' > cron.json

CRON_ID=$(jq -r '.data.id' cron.json)
```

| Field | Required | Value |
| --- | --- | --- |
| `name` | Yes | Up to 100 characters. |
| `projectIds` | Yes | The projects it fires in, all of this workspace. A project created later joins only when you edit the cron to add it. Send an empty list for a cron that runs with no repository: its run has no clones, files no session, and sends no Sensei message and raises no Sensei Event. |
| `prompt` | Yes | What the Cron Agent is asked to do, handed to it word for word. It can run a skill from the project's checkout. |
| `cronExpression` | Yes | Five fields, in order minute, hour, day of month, month and day of week, read in the Workspace Timezone. The schedule may fire at most once every 15 minutes. At a one-hour clock change, one fixed time in the skipped hour runs once, one hour later, and one fixed time in the repeated hour runs once, on the first pass. A later time in that skipped hour can be skipped when the next run is chosen after an earlier one. A time less than 15 minutes before the clock falls back can run again on the later pass. A wildcard hour keeps real spacing across that one-hour change. A shorter change can leave a longer gap. |
| `model` | No | A model id such as `anthropic:opus__xhigh`. Leave it out, or send `null`, for the workspace default. |

The response answers 201 with the cron, enabled:

```json
{
  "data": {
    "id": "0e6b2d91-…",
    "name": "Weekly dependency check",
    "projects": [{ "id": "9b1d4e27-…", "name": "storefront", "baseBranch": "main", "lastRunStatus": null, "running": false }],
    "cronExpression": "0 6 * * 1",
    "timezone": "UTC",
    "model": null,
    "enabled": true,
    "nextRunAt": "2026-10-05T06:00:00.000Z",
    "createdByName": "Ada Lovelace"
  }
}
```

With the CLI:

```bash
kosuke crons create "$WORKSPACE_ID" --project "$PROJECT_ID" --name "Weekly dependency check" \
  --prompt "Check for outdated dependencies and update the patch versions." \
  --cron-expression "0 6 * * 1"
```

Repeat `--project` to add more projects. The CLI requires `--project`, so for a cron with none, send the whole body: `kosuke crons create "$WORKSPACE_ID" --body '{"name":"Weekly digest","projectIds":[],"prompt":"Summarize the week.","cronExpression":"0 6 * * 1"}'`. `kosuke workspaces list` gives the workspace id.

A cron has no timezone of its own. Every cron of a workspace is read in the Workspace Timezone, UTC until an owner or admin sets another. The cron's `timezone` field tells you which zone that is, and you cannot send it. To change it for every cron at once:

```bash
kosuke workspaces timezone set "$WORKSPACE_ID" --timezone Europe/Zurich
```

Creating a cron answers 400 for a body that sends `timezone`, a schedule that fires more often than every 15 minutes, or a project of another workspace. It also answers 400 when the workspace has no usable [AI provider](https://kosuke.ai/docs/workspace/ai-providers.md) Credentials.

## Read its runs

Every fire leaves one run covering its selected projects, even a fire that started nothing. A cron with no project leaves one run with no project. Projects are not paused one by one. `projects` lists every project a run covers:

```bash
curl -s "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/crons/$CRON_ID/runs" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  | jq '.data.runs[] | {projects, status, summary, error, startedAt}'
```

The response holds `runs`, newest first, with `total` and `failed` counts. It takes `page` and `pageSize`, and `projectId` and `status` filters that repeat for several values, such as `?status=failed&status=skipped`.

| Run `status` | Meaning |
| --- | --- |
| `running` | The Cron Agent is working. |
| `succeeded` | The Cron Agent finished. `summary` holds the first line of its reply. |
| `failed` | The fire was refused, or the Cron Agent failed. `error` says why. |
| `cancelled` | The cron's creator or a workspace admin stopped the run. |
| `skipped` | The previous run of this cron was still working, or another fire was already starting, so nothing started. |

`kosuke crons runs get` reads one run with its reply and the sessions it filed. `kosuke crons runs messages list` reads its transcript, and `kosuke crons runs cancel` stops a run that is still going. With the CLI, list the runs with `kosuke crons runs list <workspaceId> <cronId>`, adding `--project` or `--status` to filter them.

## Run it now

Fire the cron once, whether it is paused or not, covering all its projects:

```bash
curl -s -X POST "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/crons/$CRON_ID/run" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

The request answers `{ "data": { "runs": [{ "runId": "…", "status": "running", "projectIds": ["…"], "projectId": "…" }] } }`. The one run in `runs` covers the projects with `projectIds`. `projectId` names only the first of them and is kept for older clients. `status` is the run's status when the request answers, so it can be any status in the table above. A run that ended quickly is already `succeeded`, `failed` or `cancelled`. It is `queued` when the fire had not opened the run within about ten seconds. The run fires exactly as a scheduled one does, on the workspace's Credentials, and the sessions the Cron Agent files are Sensei's, not yours. Read a run with `kosuke crons runs get <workspaceId> <cronId> <runId>` to see how it went. A cron with no project runs once, with no project. Run now is refused for a cron disabled with "No projects left" until a save picks a project. Send `{ "projectIds": [...] }` to run it in only some of its projects. With the CLI, run `kosuke crons run <workspaceId> <cronId>`, with `--project` for each project to pick.

## Pause and resume

In the web app, the cron's **Enabled** switch in **Settings** pauses or resumes it at once. The Crons list's row menu still has **Pause** and **Resume**.

Send `enabled: false` to pause the cron, and `enabled: true` to resume it:

```bash
curl -s -X PATCH "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/crons/$CRON_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

With the CLI, run `kosuke crons update <workspaceId> <cronId> --no-enabled` to pause and `--enabled` to resume.

The same request changes any other field, and only the fields you send change. `projectIds` replaces the cron's projects. A cron disabled with "No projects left" stays disabled, and `enabled: true` is refused, until a save sends at least one project; a save that sends none, such as a rename, keeps the reason. `"model": null` goes back to the workspace default. The CLI sends it with `--body '{"model": null}'`.

Five failed runs in a row disable the whole cron: it reads `enabled: false`, and `disabledReason` says why. A run that failed because Sensei was updated while it worked does not count, and only a run that succeeds sets the count back to zero. A cancelled or skipped run leaves the count as it was. Resume it with `kosuke crons update <workspaceId> <cronId> --enabled`. Resuming or saving any edit sets the count back to zero, and resuming clears the reason. A cron disabled with "No projects left" stays disabled until a save picks a project.

## Who can do what

| Action | Who |
| --- | --- |
| Create a cron | Any member of the workspace |
| Read crons and their runs | Any member of the workspace |
| Change, pause, resume, run now, stop a run or delete | The cron's creator, or a workspace owner or admin. Anyone else gets 403. |

The agent inside a chat session acts as the session's Assignee, when the Assignee sent the turn. It can create, change, run, stop and delete crons as they could. When anyone else sent the turn, it acts as the workspace, as a cron's run does.

## Delete a cron

```bash
curl -s -X DELETE "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/crons/$CRON_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

Deleting a cron also deletes its runs and their transcripts. Their files stay in Files, and the sessions its runs filed stay. Pause the cron instead if you want to keep the runs. With the CLI, run `kosuke crons delete <workspaceId> <cronId>`.

## Next steps

- [Crons](https://kosuke.ai/docs/automate/crons.md)
- [Crons reference](https://kosuke.ai/docs/endpoint-reference/crons.md)
- [Follow a turn](https://kosuke.ai/docs/api/turns.md)
