Skip to main content
A schedule runs a workflow on a timetable without anyone pressing a button. You point a schedule at a workflow’s live deployment, choose either a fixed 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.
Schedules are gated to owner/admin on every route, reads included. This is broader than most read endpoints in the API: an org member who can view workflows still cannot list or fetch schedules. If a teammate gets a 403 reading schedules, check their org role first. (Doc-blocking finding §2.9 — owner/admin required for schedules.)

How scheduling works

You never schedule a draft. A schedule references a workflow whose live deployment is run when the schedule fires.
  1. You create a schedule against a workflow that has a live deployment. ModuleX computes the first next_run_at from the current time and marks the schedule active.
  2. A background scheduler tick runs every minute or so, finds every active schedule whose next_run_at has passed, and creates a pending scheduled run for each.
  3. 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_at advances to the next occurrence.
  4. You inspect run history and stats for the schedule, and can retry any run that failed or was cancelled.
Firing happens out of band in a background worker, not inside your HTTP request. Creating a schedule returns immediately; the workflow itself runs later, on cadence.
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.
There is no /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.
Supply exactly one cadence field for the chosen type: interval_seconds for interval, cron_expression for cron. A type/field mismatch (for example schedule_type: "cron" with no cron_expression) returns 400.

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

A ScheduleResponse 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.
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-5 is 09:00 on weekdays.
  • The expression is parsed and validated server-side; anything unparseable returns 400.
  • Cron is evaluated in the schedule’s timezone (default UTC). The next occurrence is computed in local time and then stored back as UTC in next_run_at.
  • Max length is 100 characters.

How next_run_at is computed

  • The base time is last_run_at if 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_at is 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, or timezone via update recalculates next_run_at from 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.
The response is { "schedules": [ScheduleResponse...], "total": int, "limit": int, "offset": int }.
total is computed with a coarse strategy that caps at roughly 10,000 and re-runs the list query. Treat it as an approximate count for very large schedule sets, not an exact total.

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.
A null value is dropped, not applied — sending "description": null is a no-op, so you cannot clear description, input, or config back to empty through update. If every field resolves to null, the call returns 400 with {"detail": "No updates provided"}.
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.
Deleting the schedule also deletes every run record it produced and cannot be undone. To stop a schedule temporarily while keeping its history, pause it instead.

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.
The response is { "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.
The scale of success_rate is reported inconsistently across our sources: the API computes it as a percentage (for example 95.24), while the JavaScript SDK types describe it as a fraction (0.0–1.0). Read the value defensively — branch on whether it exceeds 1 — until this is reconciled.

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 failed or cancelled; any other status returns 400 with {"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 with triggered_by set to retry.
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 the mode field in the create example.
  • config — merged over the deployment’s config. Use it for execution settings such as timeout (the per-run execution timeout, which defaults to one hour) and recursion_limit.
A scheduled firing does not accept {{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’s HTTPException envelope — {"detail": <string-or-object>} — not the credits DenialEnvelope. The full error model is on Errors & status codes.

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.