Crons
A cron sends a saved prompt to one or more of a workspace's projects on a schedule. Each time it fires, Kosuke hands the prompt, word for word, to the Cron Agent in the main sandbox of every one of its projects at once, one Cron Run each. A run opens no chat session of its own. It is its own record, with its own transcript, Artifacts and Reply. Every run spends the workspace's Credentials, never a member's Subscription. Crons explains the idea and the page in the app, and Schedule recurring work walks through a first cron.
A cron belongs to its workspace, so its endpoints sit under /api/organizations/{workspaceId}/crons. kosuke workspaces list gives the workspace id. They used to sit under a project, before a cron could cover several; those endpoints are removed. Five paths answer 410 to GET, POST, PUT, PATCH and DELETE: /api/projects/{projectId}/crons, and under it /{cronId}, /{cronId}/run, /{cronId}/runs and /{cronId}/runs/{runId}. The message says to upgrade @kosuke-ai/cli, which is what a kosuke older than 5.0.0 gets on every crons command. Any other path under /api/projects/{projectId}/crons answers 404.
| Method | Path | Description |
|---|---|---|
| GET | /api/organizations/{workspaceId}/crons?projectId=&mine=&search=&page=&pageSize= | List the workspace's crons, newest first, with each one's projects, creator and last run status. A cron fails as soon as the latest run of any of its projects failed. |
| POST | /api/organizations/{workspaceId}/crons | Create a cron, enabled, with you as its creator, in one or more of the workspace's projects: each fire runs the Cron Agent in every one of them at once. Every run spends the workspace's Credentials, never a member's Subscription, so a workspace with no usable provider is refused. The model is an encoded id such as anthropic:opus__xhigh; null or left out uses the saved Workspace Default Model when its Provider has usable workspace Credentials, otherwise Kosuke's choice among usable workspace Credentials at run time. The crontab is read in the Workspace Timezone (workspaces timezone-set), and may not fire more often than every 15 minutes. A body that sends timezone is refused. |
| GET | /api/organizations/{workspaceId}/crons/{cronId} | Read one cron: its prompt, schedule, model, creator, when it runs next, and its projects, each with its last run status and whether five failed runs in a row paused it. |
| PATCH | /api/organizations/{workspaceId}/crons/{cronId} | Change a cron; only the fields sent change. projectIds replaces its projects. Any edit starts every project over: no project stays paused and every failure count goes back to zero. enabled=false pauses the cron and enabled=true resumes it; model=null goes back to the workspace default. The creator or a workspace admin only. |
| DELETE | /api/organizations/{workspaceId}/crons/{cronId} | Delete a cron with its runs, their transcripts and their Artifacts. The sessions its runs filed stay. The creator or a workspace admin only. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/artifacts?projectId=&kind=&page=&pageSize= | List the Artifacts a cron's runs published, across all of its runs, most recently changed first: each one's title, kind and version, with the run and the project it came from, and the total the filters keep. One row per Artifact: the same slug published by two runs is two rows. Rows only: a body or a file url comes from crons runs artifacts get, with the row's runId. The project and kind filters each take several values and keep an Artifact matching any of them. |
| POST | /api/organizations/{workspaceId}/crons/{cronId}/projects/{projectId}/resume | Resume one project that five failed runs in a row paused inside a cron: it takes part in the next fire and its failure count goes back to zero, while the cron's other projects keep their state. A cron that disabled itself because every project was paused is enabled again; one a member paused stays paused. The creator or a workspace admin only. |
| POST | /api/organizations/{workspaceId}/crons/{cronId}/run | Fire a cron once now, paused or not, exactly as a scheduled fire: one Cron Run per project, each in that project's main sandbox on the workspace's Credentials, and the sessions they file are Kosuke's, not yours. Every project that five failed runs in a row did not pause, or only the projects named. Answers with runs, one { projectId, runId, status } per project: status is the run's status when the request answers, after waiting up to about ten seconds for the fire: running once the run was handed to the Cron Agent, failed or skipped when the fire refused it, succeeded, failed or cancelled when the run had already ended, and queued when the fire had not opened the run yet. crons runs get reads any of the run ids. The creator or a workspace admin only. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs?projectId=&status=&page=&pageSize= | List a cron's runs, newest first: each one's project, status, the first line of its reply or its error, with the total and failed counts of the runs the filters keep. The project and status filters each take several values and keep a run matching any of them. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId} | Read one run of a cron: its project, its status, the model it ran on, the Cron Agent's reply, and the sessions it filed with each one's status and whether it waits for an answer. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId}/artifacts | List the Artifacts a run of a cron published, most recently changed first. Rows only: a body or a file url comes from reading one. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId}/artifacts/{artifactId} | Read one Artifact of a run of a cron: a markdown or html one carries its body, an image or a video a presigned url, its media type and its size. |
| POST | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId}/cancel | Stop a run of a cron that is still going, the way Stop on its page does. The run ends cancelled; the Artifacts it stored and the sessions it filed stay, and the cron's runs in its other projects go on. The creator or a workspace admin only. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId}/diagnostics | Read why a run of a cron ended as it did: its status, error, error code and stop source (member_stop, host_watch, deadline, replaced, sandbox_abort or runtime_error, with who stopped it), the main sandbox it ran in and that sandbox's agent version, when each phase happened (queued, main woken, credentials ready, agent requested, first event, done), the sandbox's own done payload, and the Cron Agent's log lines from the sandbox with secrets redacted. The log is capped at 256 KB and cleared after 30 days. The creator or a workspace admin only. |
| GET | /api/organizations/{workspaceId}/crons/{cronId}/runs/{runId}/messages | Read the Cron Agent's transcript of one run of a cron: the prompt it was given, then everything it did, with the run's status. |
What the Cron Agent does
The Cron Agent reads its project. It never writes, creates or deletes anything in the project's checkout, never creates a branch, commits or pushes, and never installs, builds or restarts services. It never asks a Clarification. When something is unclear, it decides and states its assumptions in its Reply. The only skills it reaches are the Project Skills in the checkout. A prompt that calls any other skill is passed, word for word, into a session it files.
Work that needs a code change or a pull request goes into a chat session. The Cron Agent files one in the Backlog, and a member starts it. It starts a session or sends into one only when the cron's prompt asks for exactly that. Before it files, it reads the project's open sessions, so it does not file the same work twice, and it may fold news into a Backlog session its own cron filed earlier in the project. A run may file any number of sessions.
The sessions it files are Kosuke's. Kosuke creates them and is their Assignee. These sessions run on the workspace's Credentials, whoever starts one, never on a member's Subscription, until a member reassigns one to someone. From then on it runs on that Assignee's credentials. Each runs on the model the prompt names, otherwise on the cron's. A model the workspace cannot run falls back to the cron's. In every other way they are ordinary sessions: they show on the board and may ask Clarifications. While a session is Kosuke's, every member gets its attention cues, unread and waiting on you, as its Assignee would.
Create a cron
POST /api/organizations/{workspaceId}/crons creates a cron, enabled, with you as its creator.
| Field | Type | Required | What it does |
|---|---|---|---|
name | string, 1 to 100 characters | Yes | The cron's name. |
projectIds | array of project ids, at least one | Yes | The projects the cron fires in, one Cron Run each. Every one must belong to the workspace. The list is fixed when you save: a project created later joins only when someone edits the cron to add it. |
prompt | string | Yes | What the Cron Agent is asked to do, handed to it word for word on every fire. |
cronExpression | string | Yes | Five fields, for minute, hour, day of month, month and day of week, read in the Workspace Timezone. It may fire at most once every 15 minutes. |
model | string, or null | No | A value from GET /api/models, such as anthropic:opus__xhigh. Left out or null, each run uses the saved Workspace Default Model when its Provider has usable workspace Credentials. Otherwise Kosuke chooses a model those Credentials can run. The Cron Agent runs on this model, and so does every session it files unless the prompt names another. |
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": "List outdated dependencies and open a pull request for the safe updates.", "cronExpression": "0 9 * * 1"}'
The answer is 201 with the cron:
{
"data": {
"id": "c4a81f3e-2b69-4d07-9e15-6f0b3c8d2a71",
"projects": [
{
"id": "9b1d4e27-5c3a-4f86-a0d2-7e4b8c6f1a35",
"name": "storefront",
"baseBranch": "main",
"pausedAt": null,
"pausedReason": null,
"lastRunStatus": null
}
],
"pausedProjectCount": 0,
"createdBy": "Hn2kW8xQ4pZ7cV1mB5tR9yL3sD6fG0jA",
"createdByName": "Mara Keller",
"name": "Weekly dependency check",
"prompt": "List outdated dependencies and open a pull request for the safe updates.",
"cronExpression": "0 9 * * 1",
"timezone": "Europe/Zurich",
"model": null,
"enabled": true,
"disabledReason": null,
"lastRunAt": null,
"nextRunAt": "2026-10-05T07:00:00.000Z",
"lastRunStatus": null,
"createdAt": "2026-10-01T12:30:04.512Z",
"updatedAt": "2026-10-01T12:30:04.512Z"
}
}
The same with the CLI, where --project takes one project and repeats for several:
kosuke crons create $WORKSPACE_ID --project $PROJECT_ID --project $OTHER_PROJECT_ID \
--name "Weekly dependency check" \
--prompt "List outdated dependencies and open a pull request for the safe updates." \
--cron-expression "0 9 * * 1"
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 with PUT /api/organizations/{workspaceId}/timezone (kosuke workspaces timezone-set). The cron's timezone is that zone, read-only. In the example above the workspace is on Europe/Zurich, so the 09:00 Monday fire is 07:00 UTC. A create or change whose body sends timezone answers 400, pointing to kosuke workspaces timezone-set.
The crontab is checked on every create and every change. A request answers 400 when the crontab does not have five fields or the schedule could fire twice within 15 minutes in the Workspace Timezone. That includes the days a clock change shifts. It also answers 400 when projectIds is empty or names a project of another workspace. A workspace with no usable Credentials can hold no cron, and a create there answers 400.
List and read crons
GET /api/organizations/{workspaceId}/crons lists the workspace's crons, newest first, as { "data": { "crons": [...], "total": 12 } }.
| Parameter | What it does |
|---|---|
projectId | Keeps the crons that include this project, alone or among others. |
mine | true keeps the crons you created. |
search | Keeps crons whose name or prompt contains the text, ignoring case. |
page, pageSize | The page to read. pageSize defaults to 25 and goes up to 100. |
Each cron lists its projects, ordered by name, each with its own lastRunStatus, pausedAt and pausedReason, and counts the paused ones in pausedProjectCount. The cron's own lastRunStatus is failed as soon as the latest run in any of its projects failed, and otherwise the status of its latest run.
GET /api/organizations/{workspaceId}/crons/{cronId} reads one cron, with nextRunAt. The CLI commands are kosuke crons list <workspaceId>, with --project for the project filter, and kosuke crons get <workspaceId> <cronId>.
Change a cron
PATCH /api/organizations/{workspaceId}/crons/{cronId} takes any field of the create body, plus enabled. Only the fields you send change. projectIds replaces the cron's projects and may not be empty. model: null goes back to the workspace default. Any edit starts every project over: no project stays paused and every failure count goes back to zero.
kosuke crons update $WORKSPACE_ID $CRON_ID --cron-expression "0 7 * * 1-5"
kosuke crons update $WORKSPACE_ID $CRON_ID --project $PROJECT_ID --project $OTHER_PROJECT_ID
A flag cannot send null, so the CLI resets the model with --body '{"model": null}'.
Pause and resume
enabled: false pauses a cron and enabled: true resumes it. On the CLI these are --no-enabled and --enabled. Either one clears disabledReason. Resuming also resumes every paused project and sets every failure count back to zero.
Five failed runs in a row in one project pause that project inside the cron. Its pausedAt is set and pausedReason holds the error of the run that paused it. The cron's other projects keep firing, and a scheduled fire skips the paused one. When every project is paused, the cron disables itself: enabled becomes false and disabledReason says why. Every failed run counts, before or after the Cron Agent started, except one that failed because a Deploy replaced the main sandbox. Only a run that succeeds sets the count back to zero.
When a cron's last project is deleted, the cron stays, disabled, and disabledReason is No projects left. Edit it to pick other projects, or delete it.
Resume one project
POST /api/organizations/{workspaceId}/crons/{cronId}/projects/{projectId}/resume resumes one paused project. It takes part in the next fire and its failure count goes back to zero. The cron's other projects keep their state. When the cron disabled itself because every project was paused, it is enabled again. A cron a member paused stays paused. The answer is the cron. The CLI command is kosuke crons projects resume <workspaceId> <cronId> <projectId>.
Run now
POST .../crons/{cronId}/run fires the cron once, paused or not. With no body, it runs in every project that is not paused. { "projectIds": [...] } runs it in only those projects, each of which must be one of the cron's. A paused project named there runs once and stays paused. It runs exactly like a scheduled fire, whoever asks: the Cron Agent runs in each project's main sandbox on the workspace's Credentials, and the sessions it files are Kosuke's, not yours. The answer is { "data": { "runs": [{ "projectId": "…", "runId": "…", "status": "running" }] } }, one entry per project. The request waits up to about ten seconds for the fire to land. status is the run's status when the request answers. It is running once the run was handed to the Cron Agent, and failed or skipped when the fire refused it. A run that had already ended is succeeded, failed or cancelled. It is queued when the fire had not opened the run in that time. A queued entry already carries the id its run will have. GET .../crons/{cronId}/runs/{runId} reads how a run went; for a queued one it answers 404 until the fire lands. The CLI command is kosuke crons run <workspaceId> <cronId>, with --project <projectId> repeated to pick projects.
Read runs
Every fire leaves one run per project, including one where the Cron Agent never started, so the run list shows why a project was paused. GET .../crons/{cronId}/runs lists them, newest first, as { "data": { "runs": [...], "total": 30, "failed": 2 } }. It takes page and pageSize, and two filters that each take several values as repeated keys, such as ?projectId=…&projectId=…&status=failed&status=skipped:
| Parameter | What it does |
|---|---|
projectId | Keeps the runs in these projects. |
status | Keeps the runs with these statuses: running, succeeded, failed, cancelled or skipped. |
total and failed count the runs the filters keep. On the CLI, kosuke crons runs list <workspaceId> <cronId> --project <projectId> --status failed repeats either flag for several values.
Each run carries its projectId and projectName, its status, a one-line summary that is the first line of the Cron Agent's Reply, and error. GET .../crons/{cronId}/runs/{runId} adds the model it ran on, the report that holds the Reply, and filedSessions, the sessions the run filed, each with its status and whether it waits for an answer.
status | What it means | Counts toward pausing the project |
|---|---|---|
running | The Cron Agent is working. | No. It leaves the count as it was. |
succeeded | The Cron Agent finished. | No. It sets the count back to zero. |
failed | A gate refused the fire, something broke, the run passed its 60 minutes, or a Deploy replaced the main sandbox during it. | Yes, unless a Deploy replaced the main sandbox during it. |
cancelled | The cron's creator or a workspace admin stopped it. | No. It leaves the count as it was. |
skipped | The cron's previous run in that project was still running, or a Run now met another fire of the cron that was still starting. | No. It leaves the count as it was. |
A merge that updates the checkout does not stop a run.
A run keeps what the Cron Agent did. Its page in the app shows it, and so do these commands:
- the transcript, with
kosuke crons runs messages; - the Artifacts, with
kosuke crons runs artifacts listandkosuke crons runs artifacts get; - the Reply, the
reportfield ofkosuke crons runs get; - the sessions the run filed,
filedSessionsonkosuke crons runs get.
The sessions a run filed are ordinary sessions. They show on the board and in the session lists.
Stop a run
POST .../crons/{cronId}/runs/{runId}/cancel stops a run that is still going, the way Stop on its page does. The CLI command is kosuke crons runs cancel <workspaceId> <cronId> <runId>. The run ends cancelled. The Artifacts it already stored and the sessions it already filed stay, and the cron's runs in its other projects go on. Only the cron's creator or a workspace admin can stop a run.
Delete a cron
DELETE /api/organizations/{workspaceId}/crons/{cronId} answers 204. It removes the cron with its runs, their transcripts and their Artifacts. A run still going in any of its projects is stopped first, and its key stops working at once. The sessions its runs filed are ordinary sessions and stay. The CLI command is kosuke crons delete <workspaceId> <cronId>.
A Cron Run's own key
Each run gets a key of its own, prefixed kosuke_crn_, which the Cron Agent uses through the kosuke CLI. The key is revoked when the run ends. It acts as the cron's creator, in the cron's workspace only. It reaches what the creator's API key reaches, except every delete, run diagnostics and their personal credentials. Their role decides the rest. It files Backlog sessions only (sessions create --status backlog), and Kosuke is their Assignee.
When the creator has left the workspace, the key reaches only its own project: the sessions with their messages and jobs, session search, models, Deploys and the Deploy Chat, and skills and plugins. It then edits only the title, priority and prompt of a Backlog session its own cron filed in that project.
Everything else answers 403, as it would for the creator's own key. GET /api/user/skills and GET /api/user/plugins answer the creator's, and an empty list once they have left. A Cron Run using GLM calls Z.ai directly with the workspace's GLM Coding Plan key, and one using Kimi calls Moonshot AI directly with the workspace's Kimi Code key.
Who can do what
| Action | Who |
|---|---|
| List and read crons and their runs | Any member of the workspace. |
| Create a cron | Any member, over any of the workspace's projects. They become its creator, shown in createdBy and createdByName. |
| Change, pause, resume, resume a project, run now, stop a run or delete | The creator, or a workspace owner or admin. |
The agent inside a chat session acts as the session's Assignee, when the Assignee sent the turn. It can create, change, run and stop crons as they could, but never delete one. When anyone else sent the turn, its agent token answers 403 on every cron endpoint.
Limits
- A run acts as the cron's creator, whoever fired it. The sessions it files are Kosuke's, and Kosuke is their Assignee.
- A cron runs on the workspace's Credentials only. It never falls back to a member's Subscription, so a cron keeps firing after its creator leaves.
- A cron's projects are fixed when it is saved. A project created later joins only when someone edits the cron to add it.
- A run has 60 minutes. At the limit it is stopped and ends
failed. - Runs from before the Cron Agent keep their status, summary, error and branch, but not their transcripts.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | The body does not match the schema: a required field is missing or empty, a field is too long or of the wrong type, the model is not a supported id, or projectIds is empty. |
| 400 | BAD_REQUEST | The crontab or the 15-minute floor is not met, the body sends timezone, a project belongs to another workspace, Run now names a project that is not one of the cron's or, with no body, finds every project paused, a cron with no projects is resumed, a runs filter holds a value it does not know, or the workspace has no usable Credentials. |
| 403 | FORBIDDEN | You are not a member of the workspace, you are not the cron's creator or a workspace owner or admin, or the caller is an agent token or a Cron Run's key reaching beyond what it may. |
| 404 | NOT_FOUND | The cron, run or project does not exist in the workspace. |
| 409 | CRON_RUN_STARTING | A delete met a fire that was still starting. Try again in a moment. |
Responses and errors has the full table.