interval or a cron cadence, and ModuleX fires the workflow on that cadence in the background. Each firing produces a scheduled run with its own history, status, and statistics that you can inspect or retry later.
This page is the complete reference for schedules: how to create one, the difference between interval and cron, how recurrence is computed, how to pause and resume, how runs are recorded, and every parameter, response field, and error involved. Every operation is shown once via cURL, Python, and JavaScript.
Create a schedule
Attach a cron or interval cadence to a deployed workflow.
Cron vs. interval
How each cadence is defined, validated, and advanced.
Pause and resume
Stop a schedule from firing, then bring it back.
Run history and stats
Inspect, filter, and retry the runs a schedule produced.
Looking for a step-by-step walkthrough instead of a reference? See the guide Schedule a workflow.
Before you start
Three things must be true before a schedule will run a workflow.1
The workflow has a live deployment
A schedule fires a workflow’s live deployment, not its draft. If the target workflow has no live deployment, creating the schedule fails with a
400 (not a 404). Deploy first — see Deploy & versions.2
You are an owner or admin
Every schedule endpoint — including read-only ones — requires the
owner or admin role in the organization. A plain member cannot create, read, or manage schedules. See Roles & permissions and Organizations, roles & membership.3
You authenticate with an API key and an org header
Programmatic calls use
Authorization: Bearer mx_live_… plus X-Organization-ID. See Authentication.How scheduling works
You never schedule a draft. A schedule references a workflow whose live deployment is run when the schedule fires.- You create a schedule against a workflow that has a live deployment. ModuleX computes the first
next_run_atfrom the current time and marks the schedule active. - A background scheduler tick runs every minute or so, finds every active schedule whose
next_run_athas passed, and creates a pending scheduled run for each. - Each pending run is executed in the background: ModuleX loads the deployment, merges inputs, runs the workflow, and records the result. The schedule’s
next_run_atadvances to the next occurrence. - You inspect run history and stats for the schedule, and can retry any run that failed or was cancelled.
The scheduler tick fires roughly every 30 to 60 seconds depending on deployment configuration. Treat any cadence finer than about one minute as best-effort — the effective floor on how often a schedule can fire is governed by the tick, not by the value you set. The hard minimum for an
interval is 60 seconds.The three identifiers on a scheduled run
A single scheduled execution carries three distinct IDs. Keep them separate — they answer different questions. See Workflows & runs for the full run-identity model.
When you call
getRun or retryRun, the runId argument is the scheduled-run record’s id — not the run_id and not the thread_id.
Authentication and roles
Every endpoint under/schedules shares one auth dependency. The rules below apply uniformly to creates, reads, updates, pause/resume, run history, and retries.
string
required
Bearer mx_live_… (your API key) or Bearer <clerk_jwt> (an app session token). Both are accepted.string
required
The organization the schedule belongs to. Omitting it returns
400. If you authenticate with an API key whose scope is a different org, the request returns 403.string
application/json for POST and PUT bodies.Schedule routes are not behind the credits billing gate, so they never return the flat
DenialEnvelope ({code, layer, key, current, limit, reason}). The only throttle on the schedule API itself is the standard request rate limit (429). Credits are still consumed when a scheduled run actually executes the workflow — see Credit impact. For the full error model, see Errors & status codes and Rate limiting.Base path
The schedules router is mounted at the API root with no version or service prefix./v1 or /api segment. See Base URLs & versioning.
Create a schedule
POST /schedules creates a schedule and computes its first next_run_at. The target workflow must already have a live deployment, or the call returns 400.
Success returns
200 OK with the schedule object — not 201.Request body
string (UUID)
required
The workflow to run on this schedule. The workflow must have a live deployment.
string
required
A human-readable name. Length 1–255 characters.
string
required
Either
interval or cron. Determines which cadence field is required.integer
Required when
schedule_type is interval. Minimum 60. Rejected below 60 at three layers (request validation, service validation, and a database constraint). Leave unset for cron schedules.string
Required when
schedule_type is cron. A standard 5-field cron string, max 100 characters, parsed and validated server-side. Leave unset for interval schedules.string
default:"UTC"
An IANA timezone name (e.g.
America/New_York). Max 50 characters. Validated server-side; an unknown name returns 400. Cron expressions are evaluated in this timezone.string
Optional free-text description.
object
default:"{}"
Per-run state input. Merged over the deployment’s default input on every firing — schedule input wins on conflicting keys.
object
default:"{}"
Per-run execution config (for example
timeout and recursion_limit). Merged over the deployment’s config on every firing.Example
The SDKs accept camelCase arguments (
workflowId, scheduleType, cronExpression) and convert them to snake_case on the wire. Response objects always come back with snake_case fields (workflow_id, next_run_at). The Python client is async — every call is awaited.Response
AScheduleResponse object. The wire format is snake_case throughout.
string (UUID)
The schedule’s ID.
string (UUID)
The scheduled workflow.
string (UUID)
The owning organization.
string
The schedule name.
string | null
The description, if any.
string
interval or cron.integer | null
The interval, for interval schedules.
string | null
The cron string, for cron schedules.
string
The IANA timezone the cadence is evaluated in.
object
The per-run state input.
object
The per-run execution config.
boolean
Whether the schedule is firing.
false while paused.string (ISO 8601, UTC) | null
When the schedule will next fire. Always stored in UTC.
string (ISO 8601, UTC) | null
When the schedule last fired.
null until the first firing.string | null
The status of the most recent run.
integer
Lifetime count of runs the schedule has produced.
integer
Lifetime count of succeeded runs.
integer
Lifetime count of failed runs.
string (ISO 8601, UTC)
When the schedule was created.
string (ISO 8601, UTC)
When the schedule was last updated.
Errors
Cron and interval cadence
A schedule fires either every fixed number of seconds (interval) or on a cron calendar (cron). The cadence is set at create time and can be changed with update.
- Cron
- Interval
Set
schedule_type to cron and supply a cron_expression.- The expression is a standard 5-field cron string:
minute hour day-of-month month day-of-week. For example,0 9 * * 1-5is 09:00 on weekdays. - The expression is parsed and validated server-side; anything unparseable returns
400. - Cron is evaluated in the schedule’s
timezone(defaultUTC). The next occurrence is computed in local time and then stored back as UTC innext_run_at. - Max length is 100 characters.
How next_run_at is computed
- The base time is
last_run_atif the schedule has fired before, otherwise the current time. - On create and on resume, there is no prior run, so the first/next firing is computed from now.
- For cron, the next occurrence is computed in the schedule’s timezone and converted to UTC for storage.
next_run_atis always UTC. - For interval,
next_run_at = base_time + interval_seconds, recomputed from now if that would be in the past. - Changing
schedule_type,interval_seconds,cron_expression, ortimezonevia update recalculatesnext_run_atfrom the last run.
Because the scheduler tick runs roughly every 30–60 seconds, a schedule fires at or shortly after
next_run_at, never before. Sub-minute cron fields and intervals near 60 seconds are best-effort, bounded by the tick.Read schedules
List schedules
GET /schedules returns the org’s schedules, newest first.
string (UUID)
Filter to one workflow.
boolean
Filter by active (
true) or paused (false).integer
default:"50"
Page size, 1–100.
integer
default:"0"
Items to skip. There is no auto-pagination helper — page with
limit/offset.{ "schedules": [ScheduleResponse...], "total": int, "limit": int, "offset": int }.
Get one schedule
GET /schedules/{schedule_id} returns a single ScheduleResponse. A schedule that does not exist or belongs to another org returns 404 with {"detail": "Schedule not found"}.
Update a schedule
PUT /schedules/{schedule_id} changes a schedule. Every body field is optional; only the fields you send are changed. The accepted fields are the same as create: name, description, schedule_type, interval_seconds, cron_expression, timezone, input, and config.
is_active is not an updatable field here — use pause and resume to toggle it. Changing any of schedule_type, interval_seconds, cron_expression, or timezone recalculates next_run_at. Invalid cadence or timezone values return 400.
Pause and resume
Pause and resume toggle whether a schedule fires. While paused, the scheduler tick skips it entirely.endpoint
Sets
is_active to false. The schedule stops firing until resumed. Returns the updated ScheduleResponse. There is no separate enable/disable route — resume is the enable counterpart.endpoint
Sets
is_active to true and recomputes next_run_at from now (it does not resume from where it left off). Returns the updated ScheduleResponse.Resume restarts the clock. A paused daily schedule resumed at 14:00 computes its next firing from 14:00, not from the original time. If you need the original cadence to hold, leave the schedule active and rely on its computed
next_run_at rather than pausing.Delete a schedule
DELETE /schedules/{schedule_id} removes a schedule and all of its run history, and cancels any pending executions. The response is 200 with {"message": "Schedule deleted successfully"} (not 204). A missing or cross-org schedule returns 404.
Run history and statistics
Every firing produces a scheduled-run record. These endpoints list, fetch, aggregate, and retry those runs.List run history
GET /schedules/{schedule_id}/runs lists runs for a schedule, newest first by scheduled time.
string
Filter by run status (see the status table below).
integer
default:"50"
Page size, 1–100.
integer
default:"0"
Items to skip.
{ "runs": [ScheduleRunResponse...], "total": int, "limit": int, "offset": int }. As with the schedule list, total is an approximate count. A missing schedule returns 404.
Get one run
GET /schedules/{schedule_id}/runs/{run_id} returns a single ScheduleRunResponse, where run_id is the scheduled-run record’s id. There are two 404 paths: the schedule is not in your org, or the run does not exist (or belongs to a different schedule), returning {"detail": "Run not found"}.
The ScheduleRunResponse object
string (UUID)
The scheduled-run record ID.
string (UUID)
The schedule that produced this run.
string (UUID)
The workflow that was run.
string | null
The workflow execution run ID, prefixed
sched_. Populated once execution starts.string | null
The execution thread ID, prefixed
sched_thread_.string (ISO 8601, UTC)
When the run was supposed to fire.
string (ISO 8601, UTC) | null
When execution began.
string (ISO 8601, UTC) | null
When execution finished.
number | null
completed_at − started_at, in seconds.string
The run status (see the table below).
string | null
The failure reason, when failed.
string
What triggered the run:
scheduler or retry.string (UUID) | null
The deployment that was executed.
string (ISO 8601, UTC)
When the run record was created.
Run status values
Run statistics
GET /schedules/{schedule_id}/runs/stats aggregates a schedule’s recent runs.
integer
default:"7"
The lookback window in days, 1–90.
integer
The window the stats cover.
integer
Runs scheduled within the window.
integer
Runs with status
succeeded.integer
Runs with status
failed.number
Success rate over the window. See the note below on its scale.
number | null
Mean duration over succeeded runs;
null if none qualify.number | null
Shortest succeeded-run duration;
null if none qualify.number | null
Longest succeeded-run duration;
null if none qualify.Retry a failed or cancelled run
POST /schedules/{schedule_id}/runs/{run_id}/retry re-runs a finished run by queueing a brand-new background execution. It does not run inline and does not modify the original run.
- The original run’s status must be
failedorcancelled; any other status returns400with{"detail": "Can only retry failed or cancelled runs. Current status: <status>"}. - A missing schedule or run returns
404. - On success the response is
{ "message": "Retry scheduled", "original_run_id": "<id>" }. The retry creates a fresh run withtriggered_byset toretry.
There is no public “run now” endpoint for a single schedule. To trigger an ad-hoc execution, either retry a finished run, or run the workflow directly through Run via API. The retry path queues a new background run rather than blocking on the result.
Inputs and config on each firing
Every firing merges two layers, with the schedule winning on conflicting keys:input— merged over the deployment’s default input. Use it to pin per-schedule state, like themodefield in the create example.config— merged over the deployment’s config. Use it for execution settings such astimeout(the per-run execution timeout, which defaults to one hour) andrecursion_limit.
{{node_id.field}} references in input, because there is no upstream node to reference at trigger time — the schedule is the trigger. The {{ref}} system applies inside the workflow graph at execution. For how references resolve within a run, see Variables & references.
Credit impact
The schedule API itself — create, read, update, pause/resume, run history — does not consume credits and is not behind the credits billing gate. The only throttle on these calls is the request rate limit. Credits are consumed when a scheduled firing executes the workflow, exactly as they would for a manual run: any managed-model, managed-knowledge, or other metered work inside the workflow draws down credits at execution time. A schedule that fires every minute meters the same as running that workflow manually every minute. Size the cadence with that in mind, and review Credits & metering and Usage gating & limits before scheduling a high-frequency, credit-heavy workflow.Error reference
Schedule routes use FastAPI’sHTTPException envelope — {"detail": <string-or-object>} — not the credits DenialEnvelope. The full error model is on Errors & status codes.
Related pages
Schedule a workflow (guide)
A walkthrough that takes a workflow from deployment to a live schedule.
Deploy & versions
Create the live deployment a schedule needs before it can run.
Run via API
Trigger a workflow on demand instead of on a cadence.
Roles & permissions
Why schedules require the owner or admin role.
Credits & metering
How scheduled runs consume credits when they execute.
Workflows & runs
The run-identity model behind a scheduled run’s three IDs.