Scheduled syncs: run a batch source on a cadence
A batch source can sync on a schedule instead of only when you call
POST /v1/sources/{id}/syncs. Each due tick starts one sync through exactly
the same path as a manual sync: the same checks, the same at-most-one-running
rule, the same landing, normalization, profiling and sync.completed
webhook. Schedules apply to batch sources (files, object stores and
warehouses); stream sources are consumed continuously and cannot be
scheduled.
Set a schedule
A source has at most one schedule. Two cadence forms exist, both in UTC:
| Form | Meaning |
|---|---|
{"kind": "interval", "every_hours": 1..168} | every N hours on the grid anchor + n × N; anchor (RFC 3339) is optional and defaults to the moment you create the schedule, truncated to the minute |
{"kind": "daily", "at_utc": "HH:MM"} | once a day at a fixed UTC time |
{
"schedule": { "kind": "interval", "every_hours": 6, "anchor": "2026-10-12T00:00:00Z" },
"enabled": true
}
curl -X PUT "$TESSERA_API/v1/sources/$SOURCE_ID/schedule" \
-H "Authorization: Bearer $TESSERA_KEY" -H "Content-Type: application/json" \
--data '{"schedule": {"kind": "daily", "at_utc": "02:30"}}'
Setting a schedule needs the admin scope (a schedule starts syncs on its
own); reading it needs read. The first PUT creates the schedule (a sch_
id); a later PUT replaces its cadence and enabled in place and keeps the
id and the outcome history. enabled: false pauses it: no ticks until you
enable it again. DELETE /v1/sources/{id}/schedule removes it; no further
ticks fire. Deleting the source deletes its schedule too. Any other form —
other cadence kinds, crontab expressions, time zones, every_hours outside
1–168 — is refused with invalid_argument.
A grid point at or before the moment you create the schedule never fires,
so a new schedule never runs immediately; its first run is
next_tick_at.
What each tick does
- Mode. The sync runs
incrementalwhen the source supports it — its column mapping declares acursor, or it is a file or object source whose object ledger skips objects already ingested — andfullotherwise. Watermarks advance only when a sync commits, exactly as for manual syncs. - Overlap. If a sync of the source is still queued or running when a
tick falls due, the tick starts nothing and records
skipped_overlap. A slow sync is never doubled up. - Downtime. After an outage only the latest missed tick runs; ticks are never back-filled.
- One sync per tick. Each tick starts at most one sync, even with several scheduler replicas or a restart mid-tick.
Read the outcome
GET /v1/sources/{id}/schedule (or the project-wide
GET /v1/source-sync-schedules) reports what the schedule did:
{
"schedule_id": "sch_01k7c2x9m3r8q4w6e5t7y9u0ia",
"source_id": "src_01k7c2w4n5p6q7r8s9t0v1w2xa",
"source_name": "orders",
"source_kind": "gcs",
"project_id": "proj_01k7c2v3m4n5p6q7r8s9t0v1wa",
"environment": "live",
"schedule": { "kind": "interval", "every_hours": 1 },
"enabled": true,
"next_tick_at": "2026-10-12T11:00:00Z",
"last_tick_at": "2026-10-12T10:00:00Z",
"last_outcome": "failed",
"last_sync_id": "sync_01k7c3a1b2c3d4e5f6g7h8j9ka",
"last_error": { "code": "mapping_rejection_rate_exceeded", "message": "12% of rows were rejected" },
"consecutive_failures": 2,
"created_at": "2026-10-12T08:12:00.000000Z",
"updated_at": "2026-10-12T10:00:41.000000Z"
}
last_outcome | Meaning |
|---|---|
succeeded | the tick's sync succeeded; consecutive_failures resets to 0 and last_error clears |
failed | the tick's sync failed (including a sync stopped for its timeout); last_error holds its error |
launch_failed | no sync could start: the source was disabled, has no mapping, or the sync job could not be launched; last_error.code is the same reason POST /syncs would answer. internal means the scheduler itself failed to handle the tick three times in a row; other schedules keep running meanwhile |
skipped_overlap | a sync of the source was still running; nothing started. consecutive_failures and last_error keep their values, so the newest failure stays visible until a sync succeeds |
last_sync_id names the newest sync a tick started. While that sync is
still running, last_outcome still describes the tick before it.
Schedules are never paused automatically: repeated failures only raise
consecutive_failures, so alert on that count and fix the cause.
Every scheduled sync also publishes sync.completed like any other sync.
To tell scheduled syncs from manual ones, read
GET /v1/syncs/{sync_id}/trigger (or GET /v1/sources/{id}/sync-triggers,
which pages exactly like the sync history):
{
"sync_id": "sync_01k7c3a1b2c3d4e5f6g7h8j9ka",
"source_id": "src_01k7c2w4n5p6q7r8s9t0v1w2xa",
"trigger": "scheduled",
"schedule_id": "sch_01k7c2x9m3r8q4w6e5t7y9u0ia",
"scheduled_for": "2026-10-12T10:00:00Z"
}
Manual syncs (and syncs created before scheduling existed) read
"trigger": "manual" with null schedule fields. The sync resource itself
(GET /v1/syncs/{sync_id}) is unchanged.
Every schedule in one list
GET /v1/schedules lists every schedule of the project and environment, of
every kind: source syncs (source_sync), slot exports (export),
observation-time rescore sweeps (rescore_sweep) and alert-rule evaluation
(alert_rule). It is read-only: each item's manage_href is the resource
where that schedule is created, paused, resumed, edited or deleted. Filter
with kind (comma-separated), enabled, and target_type with target_id;
pages use cursor and limit like every list, newest first. With a target
filter a page can be empty while next_cursor is set; keep following it.
{
"schedule_id": "sch_01k7c2x9m3r8q4w6e5t7y9u0ia",
"kind": "source_sync",
"environment": "live",
"target": { "type": "source", "id": "src_01k7c2w4n5p6q7r8s9t0v1w2xa", "name": "orders" },
"cadence": { "kind": "interval", "every_hours": 6 },
"enabled": true,
"pausable": true,
"next_tick_at": "2026-10-12T18:00:00Z",
"last_tick_at": "2026-10-12T12:00:00Z",
"last_outcome": {
"status": "skipped",
"code": "skipped_overlap",
"message": "",
"at": "2026-10-12T12:00:04Z"
},
"manage_href": "/sources/src_01k7c2w4n5p6q7r8s9t0v1w2xa/schedule",
"hint": null,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-12T12:00:04Z"
}
last_outcome.status is succeeded, failed or skipped; code is the
schedule's own reason. Source syncs report their sync's outcome (for example
skipped_overlap or a sync error code) and exports their newest finished
run's outcome, unless a newer tick failed before it could start a run (for
example the slot was removed), which reports tick_failed. Rescore sweeps
and alert-rule evaluation record no run outcome of their own, so they report
their newest tick: tick_completed, tick_failed (with its error) or
superseded (a later tick replaced it). An alert_rule schedule evaluates
every alert rule of its slot and cannot be paused by itself
(pausable: false, with a hint); pause or edit the rules.
next_tick_at is computed by Tessera; never recompute it.