Deploy first
A schedule fires a workflow’s live deployment, never its draft.
Create the schedule
Attach a cron cadence to the deployed workflow.
Write the cron
The 5-field cron format, timezones, and common patterns.
Monitor runs
Inspect run history and stats, and retry failed runs.
What you will build
A schedule that runs a deployed workflow automatically at a fixed calendar time — for example, a report workflow every weekday at 09:00 in your timezone. ModuleX fires the workflow’s live deployment in the background on that cadence and records each firing as a scheduled run you can inspect. This guide uses acron schedule. ModuleX also supports a fixed-interval cadence (every N seconds); the reference covers both in Cron and interval cadence.
Before you start
1
You are an owner or admin
Every schedule endpoint requires the
owner or admin role in the organization — including the read-only ones that list schedules and runs. A plain member receives a 403, even though they may be able to view the underlying workflow. See Roles & permissions and Organizations, roles & membership.2
The workflow has a live deployment
A schedule references a workflow’s live deployment, not its draft. If the workflow has no live deployment when you create the schedule, the call fails with a
400 (not a 404). Step 1 covers deploying.3
You have an API key and your organization ID
Programmatic calls authenticate with
Authorization: Bearer mx_live_… plus X-Organization-ID. Create a key from the app, or see Authentication and the Quickstart.Set your variables
The cURL examples below reuse these placeholders. Replace them with your own values; the workflow and organization IDs are UUIDs.MODULEX_API_KEY and MODULEX_ORGANIZATION_ID from the environment if you omit the arguments. The JavaScript SDK has no environment-variable fallback — pass the values explicitly.
Step 1: deploy the workflow
A schedule runs the workflow’s live deployment. Before scheduling, make sure the workflow is deployed — from the builder, or via the API. Skipping this is the most common reason aPOST /schedules returns 400 with a message about a missing live deployment.
For how deployments work and how to create one, see Deploy & versions. Once the workflow has a live deployment, continue.
Step 2: create the schedule
Create the schedule withPOST /schedules. The example below fires the workflow at 09:00 every weekday in America/New_York and pins a per-run input of {"mode": "full"}.
The SDKs accept camelCase arguments (
workflowId, scheduleType, cronExpression) and send them as snake_case on the wire. Responses always come back snake_case (workflow_id, next_run_at). Creating a schedule returns immediately with 200 OK (not 201); the workflow itself runs later, on cadence.Create request fields
These are the fields you send toPOST /schedules. For a cron schedule, supply cron_expression and leave interval_seconds unset.
string (UUID)
required
The workflow to run. It must have a live deployment, or the call returns
400.string
required
A human-readable name. Length 1–255 characters.
string
required
cron for a calendar cadence, or interval for a fixed number of seconds. This guide uses cron.string
Required when
schedule_type is cron. A standard 5-field cron string (see Cron syntax). Max 100 characters. Validated server-side; an unparseable expression returns 400.integer
Required only when
schedule_type is interval. Minimum 60. Leave unset for cron schedules.string
default:"UTC"
An IANA timezone name (for example
America/New_York). Max 50 characters. The cron expression is evaluated in this timezone. An unknown name returns 400.string
Optional free-text description.
object
default:"{}"
Per-run state input. On every firing it is merged over the deployment’s default input — schedule input wins on conflicting keys. Plain values only;
{{node_id.field}} references do not resolve at trigger time because there is no upstream node.object
default:"{}"
Per-run execution config such as
timeout (the per-run timeout, default one hour) and recursion_limit. Merged over the deployment’s config on every firing.What you get back
The response is aScheduleResponse. The key field to note now is next_run_at: ModuleX computes the first firing from the current time and marks the schedule active (is_active: true).
string (UUID)
The schedule’s ID. Use it for every follow-up call.
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 it last fired.
null until the first firing.string | null
Status of the most recent run.
integer
Lifetime count of runs produced.
0 at creation.integer
Lifetime count of succeeded runs.
integer
Lifetime count of failed runs.
workflow_id, name, schedule_type, cron_expression, timezone, input, config) plus organization_id, created_at, and updated_at. For the full field list, see the reference response section.
In the example above the schedule is created on a Sunday, so the first weekday firing at 09:00
America/New_York lands the following morning. 09:00 Eastern is stored as 13:00Z because next_run_at is always UTC.Create errors
Schedule routes use FastAPI’s
HTTPException envelope and are not behind the credits billing gate, so they never return the flat DenialEnvelope. Credits are consumed only when a scheduled run actually executes the workflow (see Cost of a schedule). For the full error model, see Errors & status codes.
Cron syntax
Acron_expression is a standard 5-field cron string, evaluated in the schedule’s timezone:
1,15), a range (1-5), a step (*/15), or * for “every”. ModuleX parses and validates the expression server-side; anything it cannot parse returns a 400. Maximum length is 100 characters.
Common patterns
Timezones and next_run_at
The cron expression is evaluated in the schedule’s timezone (default UTC). ModuleX computes the next occurrence in that local timezone and then stores it in next_run_at as UTC. So a schedule with 0 9 * * 1-5 and America/New_York shows 13:00Z (or 14:00Z during standard time) — that is the same 09:00 local moment, expressed in UTC.
When you change the cadence or timezone with an update, next_run_at is recalculated. The base for the next firing is the last run if there is one, otherwise the current time. For the exact recurrence model, see How next_run_at is computed.
Edit the cadence later
To change the cron expression, timezone, name, input, or config after creation, sendPUT /schedules/{schedule_id} with only the fields you want to change. Changing the cadence or timezone recalculates next_run_at.
A
null field in an update is dropped, not applied, so you cannot clear description, input, or config back to empty through update. is_active is not updatable here — use pause and resume instead. If every field resolves to null, the call returns 400 with {"detail": "No updates provided"}. Full update rules are in the reference.Step 3: monitor scheduled runs
Every firing produces a scheduled run record. Use the run endpoints to confirm your schedule is working and to act on failures.List the schedule’s runs
GET /schedules/{schedule_id}/runs lists runs newest-first by scheduled time. Filter by status while debugging, and page with limit (1–100, default 50) and offset.
ScheduleRunResponse. The fields most useful for monitoring:
string (UUID)
The scheduled-run record ID. This is the
run_id argument you pass to the get-run and retry endpoints.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 the run failed.
string
What triggered the run:
scheduler for a normal firing, or retry for a manual retry.string | null
The workflow execution’s run ID, prefixed
sched_. Populated once execution starts. This is distinct from the record id above.string | null
The execution thread ID, prefixed
sched_thread_.Run status values
Check the success rate
GET /schedules/{schedule_id}/runs/stats rolls up recent runs over a days window (1–90, default 7). Use it to spot a schedule that is silently failing.
Retry a failed run
If a firing failed or was cancelled, retry it withPOST /schedules/{schedule_id}/runs/{run_id}/retry, where run_id is the scheduled-run record’s id. The retry queues a brand-new background execution with triggered_by set to retry; it does not run inline and does not modify the original run.
{ "message": "Retry scheduled", "original_run_id": "<id>" }. Retrying a run whose status is anything other than failed or cancelled returns 400 with {"detail": "Can only retry failed or cancelled runs. Current status: <status>"}.
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. For the get-one-run endpoint and the full run schema, see the reference run section.
Pause or resume a schedule
To stop a schedule firing without deleting its history, pause it; bring it back with resume. While paused (is_active: false), the scheduler tick skips it.
Cost of a schedule
The schedule API itself — create, read, update, pause, resume, run history — does not consume credits. Credits are consumed when a scheduled firing executes the workflow, exactly as 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 accordingly, and review Credits & metering and Usage gating & limits before scheduling a high-frequency, credit-heavy workflow.Troubleshooting
Create returns 400 about a missing live deployment
Create returns 400 about a missing live deployment
The workflow has no live deployment yet. Deploy it first (see Deploy & versions), then create the schedule. This is a
400, not a 404.A teammate gets 403 just listing schedules
A teammate gets 403 just listing schedules
Schedule routes require the
owner or admin role on every endpoint, reads included. A plain member cannot list, read, or manage schedules. Check the teammate’s organization role — see Roles & permissions.The schedule never fires, or fires late
The schedule never fires, or fires late
Confirm
is_active is true (a paused schedule is skipped) and that next_run_at is in the future and in the expected UTC time. Remember the scheduler tick runs every 30–60 seconds, so firings land at or just after next_run_at, never before. Sub-minute cadences are best-effort.Runs show status skipped
Runs show status skipped
A
skipped run means the firing was reached but could not execute — most often because the workflow lost its live deployment between scheduling and execution. Re-deploy the workflow.The cron expression is rejected with 400
The cron expression is rejected with 400
The expression must be a valid 5-field cron string (max 100 characters) that ModuleX can parse, and the
timezone must be a valid IANA name. Re-check both against Cron syntax.Related pages
Schedules (reference)
The complete endpoint reference: every parameter, response field, and error.
Deploy & versions
Create the live deployment a schedule needs.
Run via API
Trigger a workflow on demand instead of on a cadence.
Run a workflow (REST + SDK)
Authenticate, run a workflow, and stream the result.
Roles & permissions
Why schedules require the owner or admin role.
Credits & metering
How scheduled runs consume credits when they execute.