Skip to main content

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:

FormMeaning
{"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 incremental when the source supports it — its column mapping declares a cursor, or it is a file or object source whose object ledger skips objects already ingested — and full otherwise. 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_outcomeMeaning
succeededthe tick's sync succeeded; consecutive_failures resets to 0 and last_error clears
failedthe tick's sync failed (including a sync stopped for its timeout); last_error holds its error
launch_failedno 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_overlapa 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.