Schedules
A scheduled job stores a prompt, an explicit execution policy, and a clock on a main conversation. At each due occurrence Nexus creates a new ordinary child conversation and accepts its first input atomically with the schedule advance. The main conversation may continue working. The child's original answer returns through the existing asynchronous child-result input path, which queues until the main conversation can answer.
A job's own paths start at /agent_api/v1/workspaces/:workspace_public_id/conversations/:conversation_public_id/schedules. The member plane and the parent conversation's access rules apply. Listing and reading require readable access; creating, editing, and lifecycle commands require writable access. A scheduled job is scoped to that exact conversation. The workspace-level index lists the jobs of every conversation the caller can read in that workspace; it is the overview, and each job's own doors stay under its conversation.
GET /agent_api/v1/workspaces/{workspace_id}/schedules
GET /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules
POST /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules
GET /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}
PATCH /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}
POST /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}/pause
POST /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}/resume
POST /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}/cancel
POST /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}/run
GET /agent_api/v1/workspaces/{workspace_id}/conversations/{conversation_id}/schedules/{job_id}/executions| Method | Path suffix | Result |
|---|---|---|
GET | /workspaces/{workspace_id}/schedules | {schedules: [...], pagination: {next_after}} across the workspace's readable conversations |
GET | empty | {schedules: [...], pagination: {next_after}} |
POST | empty | 201 {schedule: {...}}; requires Idempotency-Key |
GET | /:public_id | {schedule: {...}} |
PATCH | /:public_id | Optimistic edit; requires schedule.expected_lock_version |
POST | /:public_id/pause | Pause future occurrences |
POST | /:public_id/resume | Resume at the next future occurrence |
POST | /:public_id/cancel | Cancel future occurrences and any still-queued last execution |
POST | /:public_id/run | Start one execution now, outside the clock |
GET | /:public_id/executions | {executions: [...], pagination: {next_after, last_cursor}} |
Intent and rules
Create and edit use a schedule envelope:
{
"schedule": {
"name": "Morning mail",
"prompt": "Summarize unread email and report the important items.",
"rule": {"kind": "daily", "local_time": "09:00", "time_zone": "Asia/Shanghai"},
"model": {"model": "provider/model", "reasoning_enabled": true, "reasoning_effort": "medium"},
"configuration": {},
"tool_names": ["mail_search"],
"approval_mode": "ask"
}
}prompt and rule are required at creation. name is optional and at most 255 characters. Optional answering_user_public_id addresses an eligible Agent; omission uses the parent's answerer. Optional speaker_public_id preserves an ingress speaker controlled by the creator. configuration, tool_names, and approval_mode have the ordinary input door's validation and narrowing rules. An empty tool list remains empty. A null tool restriction uses the profile's current declaration at execution.
An omitted model.model resolves from the selected answerer. On a conversation with captured execution defaults and the same answerer, it captures that conversation's current model and selection intent. Ordinary conversations and an explicitly different answerer resolve the selected Agent's default. An explicit model.model, even when equal to the default, records an explicit route constraint unless the request supplies its own model_selection object.
model_selection and fallback_models use the same shapes as input model selection. Creation captures the selected configuration's ordered fallback list when the list is omitted or null. [] disables fallback; a supplied list replaces the whole list. The saved model, intent and fallback list govern each occurrence, including after the parent's model or Preset changes. A same-answerer scheduled child also retains its parent's captured execution declaration and memory limit.
On edit, omitted policy fields keep their saved values. A supplied model_selection object replaces the intent. Naming a complete model reference without an intent records a new explicit route constraint; changing only reasoning controls preserves the intent. A fallback_models array replaces the list, [] disables it, and null captures the selected configuration's default list again.
The model's independent reasoning_enabled and reasoning_effort controls follow Models. An edit may send only model: {reasoning_enabled: false} to change the switch while preserving the saved reference and effort. Omitted or null controls preserve the current selection unless the edit names a different model, which starts from its own defaults. The job's model projection is saved intent; the child reply's model projection reports the effective selection at execution.
source_run_public_id and source_task_key, when supplied together at creation, identify a task hosted by the main conversation. These immutable weak references record provenance. Their old execution does not own the job's future lifetime, and stopping it does not cancel the schedule.
There are three rule shapes:
| Kind | Fields | Meaning |
|---|---|---|
once | run_at | One future ISO 8601 instant with explicit UTC offset |
interval | every_seconds, starts_at | Anchored integer interval from 60 through 31,536,000 seconds |
daily | local_time, time_zone | HH:MM in an IANA time zone |
An initial or explicitly reset clock must land within the ordinary input future bound of ten years. Timestamp rules normalize to UTC with up to six fractional digits. Daily rules skip a local time that does not exist during a daylight saving gap; a repeated local time uses its first occurrence once. Resuming an expired one-time rule requires creating a new job or editing its rule while it is paused. Completed and canceled jobs cannot be edited or resumed.
Dispatch and management
A fixed minutely sweep discovers indexed due rows with a bounded cursor. A lost enqueue is recovered by later sweeps. The parent conversation row serializes dispatch against edits, pause, and cancel. Repeated delivery of the same due occurrence creates one child. After downtime, missed occurrences coalesce into one execution and the clock advances to the next future occurrence.
A job has at most one unfinished execution. If its previous child still has its scheduled input queued or its original execution live, a due recurrence advances without starting another child and records last_error_code: "execution_in_progress". Each new child inherits the parent's current memory context, access carrier, runner, and billing subject. The saved prompt, model, tool restriction, approval mode, and answerer remain the explicit job intent. Changed authority or an unavailable input policy cannot silently broaden that intent. Later exchanges or manual regeneration in a reusable child do not hold the schedule's next occurrence. A side conversation cannot own a scheduled job.
Pause affects future occurrences and leaves an already accepted execution alone. Cancel also removes the last child's scheduled input if it is still queued. If that input has already become a turn, the running child remains under the ordinary conversation/run stop controls. Repeating pause, resume, or cancel when it is already in that state is idempotent. A stale edit returns 409 stale_object; a forbidden edit or resume of a terminal schedule returns 422 schedule_finished.
run starts one execution now: the same child birth under the job's saved prompt, model, tool restriction, approval mode, answerer, and the creator's standing, with scheduled_for set to the request time. It is available on an active or paused job and needs write standing on the parent. The rule's next_run_at and the job's status do not change, so a recurring job still runs at its next occurrence and a one-time job stays pending. The one-unfinished-execution rule applies: while the previous child is still queued or live, run returns 409 execution_in_progress and records that code as last_error_code. A canceled or completed job returns 422 schedule_finished. The response is the job with its new last_execution.
The schedule states are active, paused, canceled, and completed. completed means a one-time clock has been dispatched; its child can still be queued, running, blocked, failed, or completed. Execution failure belongs to that child and does not imply that a recurring schedule is canceled.
Execution projection and callbacks
The job resource includes its UUIDs, saved model_selection and fallback_models, the owning conversation's current conversation_title (nullable, for lists across conversations), status, lock_version, next_run_at, last_enqueued_at, last_input_public_id, last_error_code, last_execution, and creation/update timestamps. The deterministic response fixtures are in contracts/nexus/v1/schedules.json.
last_execution and the execution list follow the caller's current access to each child and omit deleted children. A hidden latest child makes last_execution null; the job's own accepted-input snapshot remains unchanged.
Each execution row contains child_conversation_public_id, input_public_id, turn_public_id, run_public_id, scheduled_for, status, and created_at. The input UUID is present from atomic birth. Before materialization, the turn and run UUIDs are null. A tool-less reply has a turn and model invocation but may never have a run. Those missing identities remain null. scheduled_for is the nominal occurrence, while created_at is the actual child birth time.
Both lists use the API's bounded opaque keyset pagination. Execution listings also return last_cursor for the last row, including a non-overflowing page; an empty page preserves the requested after checkpoint. This supports polling for new executions without replaying old children. Poll the child itself to observe changes to an already discovered execution.
The callback retains the original scheduled answer, including a provider fallback; later manual edits or regeneration do not substitute their text. The pending report retains its execution details until relay succeeds. Acceptance and the child turn's relay stamp commit together, so repeated recovery does not enqueue duplicate reports. Queue saturation or temporarily unavailable authority leaves the report available for retry. After execution detail retention expires, a provider fallback's durable result still determines the execution projection.
The callback's callback_result fixes that worker's conversation, accepted input, turn and exact final variant UUIDs, plus its original requester actor. Materialized callbacks retain the descriptor in the turn's callback_sources; the worker's input UUID remains its independent task ID. Compatible worker finals can share one parent report. Each source is checked before consumption; stopping one source afterward does not cancel that combined report. External per-worker delivery uses its own fixed final, while the combined report remains in the main conversation. See kernel mail for the batching and provenance contract.
The model-facing <task_result> envelope names the job and child UUIDs, the result status, and the child's nominal scheduled_for in UTC ISO 8601 with six fractional digits. Before the answer, a <prompt> line carries the first 80 characters of the retained original accepted request, escaped by the same rule as the answer. This source brief remains available even when job creation was a command with no message in the main conversation's history. Later edits to the job's prompt or rule do not change an accepted occurrence's source. The brief is a bounded identity hint, not the complete task or a guarantee that its one-time or recurring intent appears within those characters; scheduled_for is not the actual execution or completion time.
A repairable needs_attention hold has no final callback yet. Repairing that original execution can still produce its result. If an edit replaces a held original, the old execution is stopped and its pending callback follows the ordinary source-stop rule. Deleting the receiving main conversation or its workspace releases the pending report's retention obligation.
The callback's durable source stamp names the actual child execution run, when one exists. It never names the old run that created the job. The child conversation UUID remains the precise source for a tool-less execution. The main conversation consumes callbacks through its ordinary input door; callback acceptance itself does not bypass an ongoing reply.
The execution child remains reusable. A later queued send from its parent opens a separate reply obligation, correlated to that sending run and task and returned on its answerer, model, tools, approval and wake policy. It does not replace the scheduled answer or keep the occurrence unfinished. A steer joins the existing turn and its original obligation. A person's independent exchange, a third-party request, or a grandchild's callback stays local to the child; none automatically becomes another scheduled report to the main conversation.