Scheduled Jobs on Tawa

The Short Version

Declare schedules in catalog-info.yaml. iec-cron POSTs to your endpoint on schedule. Return 200 immediately — do slow work asynchronously.

spec:
  schedules:
    - name: nightly-sync
      cron: "0 2 * * *"
      endpoint: /internal/cron/nightly-sync
      timezone: America/Denver
      timeoutMs: 300000

Handler Pattern

// Return 200 immediately — iec-cron must not wait for slow work
app.post('/internal/cron/nightly-sync', async (req, res) => {
  res.json({ success: true })

  // Do work asynchronously after responding
  doSlowWork().catch((err) => logger.error({ err }, 'Nightly sync failed'))
})

// For fast work (< timeoutMs), you can do it inline:
app.post('/internal/cron/hourly-check', async (req, res) => {
  await runQuickCheck()
  res.json({ success: true })
})

Headers iec-cron Sends

POST {your-service-url}{endpoint}
X-Schedule-Name: nightly-sync
X-Cron-Expression: 0 2 * * *
X-Fired-At: 2026-01-15T07:00:00.000Z

Cron Syntax

┌──────── minute (0-59)
│ ┌────── hour (0-23)
│ │ ┌──── day of month (1-31)
│ │ │ ┌── month (1-12)
│ │ │ │ ┌ day of week (0-7, both 0 and 7 = Sunday)
│ │ │ │ │

"*/15 * * * *"      every 15 minutes
"0 * * * *"         top of every hour
"0 0 * * *"         daily at midnight UTC
"0 9 * * 1-5"       weekdays at 9am
"0 2 1 * *"         1st of each month at 2am

Only 5-part cron expressions. 6-part (with seconds) is NOT supported.

Schedule Fields

FieldRequiredDefaultDescription
nameYesUnique name in your namespace
cronYes5-part cron expression
endpointYesPath in your service
timezoneNoUTCIANA timezone string
timeoutMsNo30000HTTP callback timeout
descriptionNoHuman description

Gas

EnvironmentCost per execution
Production2 tokens
Sandbox0 tokens (free)

What NOT to Do

# ❌ WRONG: cron endpoints listed in routes (they must be internal-only)
spec:
  routes:
    - path: /internal/cron/nightly-sync   # now exposed through Janus

# ✅ CORRECT: cron endpoints only in schedules
spec:
  schedules:
    - name: nightly-sync
      endpoint: /internal/cron/nightly-sync

# ❌ WRONG: /api/ prefix on cron endpoints
endpoint: /api/cron/nightly-sync   # suggests it's a public API route

# ✅ CORRECT
endpoint: /internal/cron/nightly-sync

# ❌ WRONG: 6-part cron (with seconds)
cron: "0 0 2 * * *"   # second field not supported

# ✅ CORRECT
cron: "0 2 * * *"
// ❌ WRONG: blocking the response with slow work
app.post('/internal/cron/nightly-sync', async (req, res) => {
  await doSlowWork()   // iec-cron waits, times out, marks failure
  res.json({ ok: true })
})

// ✅ CORRECT: respond immediately
app.post('/internal/cron/nightly-sync', async (req, res) => {
  res.json({ ok: true })
  doSlowWork().catch(logger.error)
})

Key Facts

  • iec-cron polls Koko every minute — new schedules go live within 60 seconds of deploy
  • Schedule names must be unique within your namespace
  • Non-2xx response or timeout = recorded as failure in run history
  • Run history viewable in tawa-web: Console → Infrastructure → Schedules
  • Septor events are written automatically for every cron execution (platform handles this)

Last updated: February 28, 2026