---
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.