← Back to Skills

Scheduled Jobs on Tawa

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

Ruleconfigure

About

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

Skill Content

This is the raw markdown that gets installed as a Claude Code rule.

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

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

## Handler Pattern

```typescript
// 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 })
})
```

## How iec-cron Calls Your Service

iec-cron POSTs the callback through Janus at `{JANUS_URL}/i/{serviceName}{endpoint}`, where
`serviceName` is your namespace with the environment suffix stripped (e.g. `my-svc-prod` →
`/i/my-svc/internal/cron/...`). The service JWT is passed as `Authorization: Bearer`.

```
POST {JANUS_URL}/i/{serviceName}{endpoint}
Authorization: Bearer {service-jwt}
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

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `name` | Yes | — | Unique name in your namespace |
| `cron` | Yes | — | 5-part cron expression |
| `endpoint` | Yes | — | Path in your service |
| `timezone` | No | UTC | IANA timezone string |
| `timeoutMs` | No | 30000 | HTTP callback timeout |
| `description` | No | — | Human description |

## Gas

| Environment | Cost per execution |
|-------------|-------------------|
| Production | 5 tokens |
| Sandbox | 0 tokens (free) |

Gas is charged upfront when the schedule fires, before your callback is invoked. Sandbox
suppresses fires entirely and charges no gas.

## What NOT to Do

```yaml
# ❌ 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 * * *"
```

```typescript
// ❌ 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 30 seconds — new schedules go live within 30 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)

Install

Copy the skill content and save it to:

~/.claude/rules/tawa-cron.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-cron

Details

Format
Rule
Category
configure
Version
1.0.63884
Tokens
~969
Updated
2026-06-24
platformcronschedulesscheduleriec-cronrecurring