--- title: "Schedule and trigger a Worker" description: "Start a Worker on a cron schedule or from a signed webhook, map webhook fields into run inputs, preview the next fire times in your timezone, and re-acknowledge a stale cost in place." --- A **trigger** starts a run of a Worker's published revision on its own: on a cron **schedule**, or when a signed **webhook** arrives. It does nothing else. There is no step engine behind a trigger. Every fire goes through the same start-run path as a manual start, with the same pinned revision, cost acknowledgement, plan limits and failure reasons (ADR-0107, extended by ADR-0135). A Workflow still starts nothing on its own. Triggers belong to Workers. ## Plans Schedules and webhooks need a paid plan. Read the flags from your entitlement: ```ts const { entitlement } = await obol.workers.entitlements(); entitlement.schedules_allowed; // false on the Free plan entitlement.webhooks_allowed; // false on the Free plan ``` The Free plan also caps a run at 30 minutes and includes 5 environment-hours a month. Both caps refuse a start at admission rather than cutting a run short. ## Create a schedule A trigger stores the hard stop you were shown, the same acknowledgement a manual start carries. A trigger that cannot present a current acknowledgement refuses to start runs rather than skipping the cost card. ```http POST /api/v1/workspaces/{workspace_id}/workers/{worker_id}/triggers Content-Type: application/json { "kind": "schedule", "cron": "0 7 * * 1-5", "timezone": "Europe/London", "overlap_policy": "skip", "acknowledged_hard_stop": { "max_total_micros_usd": 500000, "deadline_seconds": 900 }, "default_inputs": { "city": "London" } } ``` - `cron` has five fields: minute, hour, day of month, month, day of week. `0` and `7` both mean Sunday. - `timezone` is an IANA name. The schedule is evaluated in that zone, so `0 7 * * 1-5` is 07:00 London time on weekdays, across daylight-saving changes. A local time that does not exist on a spring-forward day is skipped. One that happens twice on the fall-back day fires twice. - `overlap_policy` is `skip`, `queue` or `cancel_previous`, and decides what happens when the previous run is still going. - `default_inputs` are fixed inputs every fire starts with. They are validated against the Worker's input schema (below). **Day of month and day of week both have to match.** When both fields are restricted, Obol fires only on days that satisfy both. For example, `0 9 1 * 1` fires at 09:00 on a first of the month that is also a Monday. Standard Unix cron fires when **either** matches. Restrict only one of the two fields if you want the usual behaviour. ### Preview before you save ```http POST /api/v1/workspaces/{workspace_id}/worker-triggers/schedule-preview Content-Type: application/json { "cron": "0 7 * * 1-5", "timezone": "Europe/London" } ``` The response lists the next three fire times, computed with the same matching rule the scheduler uses. A saved trigger also shows its next three fire times. ## Create a webhook ```http POST /api/v1/workspaces/{workspace_id}/workers/{worker_id}/triggers Content-Type: application/json { "kind": "webhook", "acknowledged_hard_stop": { "max_total_micros_usd": 500000, "deadline_seconds": 900 }, "input_mapping": { "issue_number": "/issue/number", "repository": "/repository/full_name" } } ``` The response carries the webhook's URL and its signing secret. Send events to `POST /api/v1/worker-webhooks/{trigger_id}`, signed in the Stripe-shaped `t=,v1=` form, in the `x-obol-signature` header (`stripe-signature` is also read). A signature more than five minutes old is refused. - `input_mapping` maps an input field to an [RFC 6901 JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) into the signed body. The body is parsed only after the signature verifies, and only when a mapping exists. - A mapped value that looks like a credential is refused. Credentials never travel as run inputs. - An error names the field and the rule, never the value. - The chosen fields become the run's inputs, so they reach the model. Map only what the job needs. ## Run inputs A Worker revision may declare a small, closed input schema: at most 20 flat fields, each a string, number, integer or boolean. Every start is validated against it before the run exists: a manual start, an API start, a schedule, a webhook, a demo or a retry. A start whose inputs fail is refused with `422 run_input_invalid`. ```http GET /api/v1/workspaces/{workspace_id}/workers/{worker_id}/input-schema?revision_id={revision_id} ``` ## When a trigger stops firing Publishing a revision can change what a run may cost. A schedule or webhook whose stored acknowledgement no longer matches the current hard stop refuses to start runs, with `schedule_estimate_stale` or `webhook_estimate_stale`. Trigger lists show this as `needs_acknowledgement: true`, and publishing warns you which schedules will need it. Re-acknowledge in place. There is no need to delete and recreate the trigger: ```http PATCH /api/v1/workspaces/{workspace_id}/workers/{worker_id}/triggers/{trigger_id} Content-Type: application/json { "acknowledged_hard_stop": { "max_total_micros_usd": 600000, "deadline_seconds": 900 } } ``` The same route re-enables, reschedules, changes the timezone or overlap policy, or re-maps inputs. Every field is optional, and sending `null` for `input_mapping` or `default_inputs` clears it. Retiring or disabling a Worker disables its triggers. Creating, changing and deleting a trigger are audited. ## Starting runs from your own scheduler You can still start runs from your own scheduler through the API. Each start echoes a fresh estimate: ```ts import { createClient } from "@obol/sdk"; const obol = createClient({ baseUrl: process.env.OBOL_CONTROL_URL!, sessionToken: process.env.OBOL_SESSION_TOKEN!, workspaceId: process.env.OBOL_WORKSPACE_ID!, }); const started = await obol.workers.run("site-sweep", { inputs: { pages: ["https://www.example.test/"], check_links: true }, confirm: (estimate) => estimate.hard_stop.max_total_micros_usd <= 3_000_000, }); ``` Obol pins each run to an immutable revision and stores its inputs. A scheduler that fires twice starts two runs, and Obol does not deduplicate scheduled intent it never received. With your own scheduler, check for an in-flight run first; a built-in trigger applies its `overlap_policy` instead.