# Use cases

Real jobs for real teams. Every scenario below is a complete job you can send to `POST /alarms` after swapping in your own URLs and [credentials](/docs#credentials). The services are real, and so are their response shapes. The [docs](/docs) explain each field and the [template language](/docs#templates), and [Integrations](/integrations) lists every built-in step.

Schedules run in the time zone you give them, so 09:00 in Berlin stays 09:00 in Berlin when the clocks change.

## For engineering teams

### The standup digest

*A platform team in Berlin wants open pull requests in `#eng` before standup, without anyone maintaining a bot.*

At 09:00 Berlin time on weekdays, list open PRs, turn each one into a line, and post them to Slack.

```json
{
  "name": "Open PRs to #eng",
  "cron": { "expression": "0 9 * * 1-5", "time_zone": "Europe/Berlin" },
  "nodes": [
    { "id": "prs", "name": "Open PRs",
      "github_list_pull_requests": { "credential_id": "YOUR_GITHUB_CREDENTIAL_ID", "owner": "acme", "repo": "web", "state": "open" } },
    { "id": "lines", "name": "One line per PR",
      "transform": { "template": "{ \"count\": \"{{ $.length }}\", \"lines\": { \"{{#each $}}\": \"• <{{ $.html_url }}|#{{ $.number }}> {{ $.title }} ({{ $.user.login }})\" } }" } },
    { "id": "post", "name": "Post to #eng",
      "slack_send_message": { "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX", "text": "*{{ $.count }} open pull requests*\n{{ join($.lines, \"\\n\") }}" } }
  ]
}
```

### Who's on call today

*An SRE team rotates on-call weekly in PagerDuty, but people still ask "who's on today?" in Slack.*

Every morning at 08:00 New York time, ask PagerDuty who's first in line for the escalation policy and announce it.

```json
{
  "name": "On-call announcement",
  "cron": { "expression": "0 8 * * *", "time_zone": "America/New_York" },
  "nodes": [
    { "id": "oncall", "name": "Current on-call",
      "pagerduty_list_on_calls": { "credential_id": "YOUR_PAGERDUTY_API_KEY_ID", "escalation_policy": "PABC123", "limit": 1 } },
    { "id": "post", "name": "Post to #sre",
      "slack_send_message": { "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX", "text": ":rotating_light: On call today: *{{ $.oncalls[0].user.summary }}* ({{ $.oncalls[0].schedule.summary }})" } }
  ]
}
```

### Know when your API is down

*A two-person startup wants to hear their API is down before their customers tell them.*

Check the health endpoint every minute, then ping [Healthchecks.io](https://healthchecks.io). A failing check stops the job before the ping, so Healthchecks notices the missing ping and alerts you by email, SMS or Slack.

```json
{
  "name": "API heartbeat",
  "cron": { "expression": "* * * * *" },
  "nodes": [
    { "id": "health", "name": "API health",
      "request": { "method": "GET", "endpoint": "https://api.acme.com/health" } },
    { "id": "ping", "name": "Tell Healthchecks",
      "healthchecks_ping": { "ping_url": "https://hc-ping.com/5f3c9a1e-8b2d-4c6f-9e7a-1d2b3c4d5e6f" } }
  ]
}
```

Every status code also lands in the job's run history.

### Trigger the nightly backup

*A team's database backup lives behind an internal endpoint, and nobody notices when the cron box that calls it dies.*

At 03:00 UTC, start the backup with a shared secret, then post the result to Discord.

```json
{
  "name": "Nightly backup",
  "cron": { "expression": "0 3 * * *" },
  "nodes": [
    { "id": "backup", "name": "Start backup",
      "request": { "method": "POST", "endpoint": "https://api.acme.com/internal/backups", "credential_id": "YOUR_BACKUP_SECRET_ID" } },
    { "id": "notify", "name": "Post to #ops",
      "discord_send_message": { "webhook_url": "https://discord.com/api/webhooks/123456789012345678/abcDEF_ghi-JKL", "content": "Backup {{ $.id }} started: {{ $.size_mb }} MB queued." } }
  ]
}
```

The endpoint should return `202` right away and do the work in the background, since each request has 10 seconds.

## For revenue and operations

### Chase failed payments

*A SaaS founder loses a few customers each month to expired cards and only notices at month-end.*

Each weekday at 09:00 New York time, list Stripe subscriptions that are `past_due` and post them to `#revenue`, where someone can reach out the same day.

```json
{
  "name": "Past-due subscriptions to #revenue",
  "cron": { "expression": "0 9 * * 1-5", "time_zone": "America/New_York" },
  "nodes": [
    { "id": "subs", "name": "Past-due subscriptions",
      "stripe_list_subscriptions": { "credential_id": "YOUR_STRIPE_KEY_ID", "status": "past_due", "limit": 100 } },
    { "id": "lines", "name": "One line each",
      "transform": { "template": "{ \"count\": \"{{ $.data.length }}\", \"lines\": { \"{{#each $.data}}\": \"• <https://dashboard.stripe.com/customers/{{ $.customer }}|{{ $.customer }}>: <https://dashboard.stripe.com/invoices/{{ $.latest_invoice }}|unpaid invoice>\" } }" } },
    { "id": "post", "name": "Post to #revenue",
      "slack_send_message": { "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX", "text": "*{{ $.count }} subscriptions are past due*\n{{ join($.lines) }}" } }
  ]
}
```

Use a Stripe [restricted key](https://docs.stripe.com/keys#limit-access) with read-only access to subscriptions.

### The morning balance email

*A founder checks the Stripe dashboard every morning for one number.*

At 07:30 their time, read the Stripe balance and send it from their own Gmail account.

```json
{
  "name": "Stripe balance email",
  "cron": { "expression": "30 7 * * *", "time_zone": "America/Chicago" },
  "nodes": [
    { "id": "balance", "name": "Stripe balance",
      "stripe_retrieve_balance": { "credential_id": "YOUR_STRIPE_KEY_ID" } },
    { "id": "email", "name": "Email me",
      "smtp_send_email": { "credential_id": "YOUR_GMAIL_CREDENTIAL_ID", "to": "founder@acme.com",
        "subject": "Stripe balance for {{ format(now:America/Chicago, \"dddd, MMM DD\") }}",
        "text": "Available: {{ $.available[0].amount / 100 }} {{ $.available[0].currency }}\nPending: {{ $.pending[0].amount / 100 }} {{ $.pending[0].currency }}" } }
  ]
}
```

Gmail needs an [app password](https://support.google.com/accounts/answer/185833) in the SMTP credential.

### New customers into the CRM

*A sales team wants every new paying customer in their CRM within 15 minutes, without paying for an integration platform.*

Fetch recent Stripe customers and upsert them by email. The request body uses the previous step's output directly, so this job needs no transform.

```json
{
  "name": "Stripe customers to CRM",
  "cron": { "expression": "*/15 * * * *" },
  "nodes": [
    { "id": "customers", "name": "Recent customers",
      "stripe_list_customers": { "credential_id": "YOUR_STRIPE_KEY_ID", "limit": 100 } },
    { "id": "upsert", "name": "Upsert contacts",
      "request": { "method": "POST", "endpoint": "https://crm.example.com/api/contacts/batch-upsert", "credential_id": "YOUR_CRM_TOKEN_ID",
        "body": "{ \"contacts\": { \"{{#each $.data}}\": { \"email\": \"{{ $.email }}\", \"name\": \"{{ $.name }}\", \"stripe_id\": \"{{ $.id }}\" } } }" } }
  ]
}
```

Use an upsert endpoint, since each run sends the latest 100 customers again.

### Month-end numbers in the board sheet

*An operations lead pastes the same three numbers into a Google Sheet on the first of every month.*

On the 1st at 08:00, fetch last month's report and send only those numbers to a Zapier catch hook that appends a row. `revenue` and `signups` arrive as numbers, not strings, because each placeholder is the whole value.

```json
{
  "name": "Monthly metrics to sheet",
  "cron": { "expression": "0 8 1 * *", "time_zone": "Europe/London" },
  "nodes": [
    { "id": "report", "name": "Last month",
      "request": { "method": "GET", "endpoint": "https://analytics.example.com/api/reports/last-month", "credential_id": "YOUR_ANALYTICS_TOKEN_ID" } },
    { "id": "pick", "name": "Pick numbers",
      "transform": { "template": "{ \"month\": \"{{ format(now() - months(1), \\\"YYYY-MM\\\") }}\", \"revenue\": \"{{ $.totals.revenue }}\", \"signups\": \"{{ $.totals.signups }}\" }" } },
    { "id": "append", "name": "Append row",
      "request": { "method": "POST", "endpoint": "https://hooks.zapier.com/hooks/catch/123456/abcdef/", "body": "{{ $ }}" } }
  ]
}
```

## For online stores

### Prices in the shopper's currency

*A US skate shop sells to Europe and the UK, and its EUR and GBP prices drift whenever exchange rates move.*

Every weekday after the European Central Bank publishes its reference rates, fetch them from the free [Frankfurter](https://www.frankfurter.app) API and send them to the store's currency settings.

```json
{
  "name": "Daily exchange rates",
  "cron": { "expression": "30 16 * * 1-5", "time_zone": "Europe/Berlin" },
  "nodes": [
    { "id": "rates", "name": "ECB rates",
      "request": { "method": "GET", "endpoint": "https://api.frankfurter.app/latest?from=USD&to=EUR,GBP,CAD" } },
    { "id": "update", "name": "Update store",
      "request": { "method": "PUT", "endpoint": "https://shop.example.com/admin/api/currencies", "credential_id": "YOUR_STORE_TOKEN_ID",
        "body": "{ \"base\": \"USD\", \"as_of\": \"{{ $.date }}\", \"rates\": { \"EUR\": {{ $.rates.EUR }}, \"GBP\": {{ $.rates.GBP }}, \"CAD\": {{ $.rates.CAD }} } }" } }
  ]
}
```

For a single pair, the built-in [`frankfurter_latest`](/integrations/frankfurter) step needs no URL at all.

### Black Friday at midnight

*A small store wants its Black Friday sale to start at exactly 00:00 Eastern, not whenever someone wakes up and flips the switch.*

Two one-time jobs: one starts the sale at midnight Eastern on November 27, and one ends it after Cyber Monday. Each fires once and switches itself off, so nothing repeats next year.

```json
{
  "name": "Black Friday 2026: start",
  "run_at": "2026-11-27T00:00:00-05:00",
  "nodes": [
    { "id": "start", "request": { "method": "POST", "endpoint": "https://shop.example.com/admin/api/sales/black-friday/start", "credential_id": "YOUR_STORE_TOKEN_ID" } },
    { "id": "tell", "slack_send_message": { "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX", "text": "Black Friday sale is live :tada:" } }
  ]
}
```

```json
{
  "name": "Black Friday 2026: end",
  "run_at": "2026-11-30T23:59:00-05:00",
  "nodes": [
    { "id": "end", "request": { "method": "POST", "endpoint": "https://shop.example.com/admin/api/sales/black-friday/end", "credential_id": "YOUR_STORE_TOKEN_ID" } }
  ]
}
```

### Warm the cache before the morning rush

*A news site's homepage is slow on the first request after a deploy or a quiet night.*

Fetch the heaviest route every 5 minutes, so readers never pay for a cold cache.

```json
{
  "name": "Warm homepage cache",
  "cron": { "expression": "*/5 * * * *" },
  "nodes": [
    { "id": "warm", "request": { "method": "GET", "endpoint": "https://news.example.com/api/home?revalidate=1" } }
  ]
}
```

## For field teams and clinics

### The rain check

*A landscaping crew in Portland decides every morning whether to mow or to do indoor jobs. The foreman checks three weather apps.*

At 05:30 Portland time, Monday to Saturday, fetch the forecast from the free [Open-Meteo](https://open-meteo.com) API and send it to the crew's Telegram group before the trucks leave.

```json
{
  "name": "Crew weather brief",
  "cron": { "expression": "30 5 * * 1-6", "time_zone": "America/Los_Angeles" },
  "nodes": [
    { "id": "forecast", "name": "Forecast",
      "open_meteo_forecast": { "latitude": "45.52", "longitude": "-122.68", "units": "imperial" } },
    { "id": "text", "name": "Tell the crew",
      "telegram_send_message": { "credential_id": "YOUR_TELEGRAM_BOT_ID", "chat_id": "-1001234567890",
        "text": "Today in Portland: {{ $.daily.precipitation_sum[0] }} in of rain, {{ $.daily.temperature_2m_min[0] }}–{{ $.daily.temperature_2m_max[0] }}°F. Wind now {{ $.current.wind_speed_10m }} mph." } }
  ]
}
```

The bot token lives in a credential, so it never appears in the job.

### Tomorrow's appointment reminders

*A dental clinic in London has patients forget appointments, and the front desk spends its evenings on reminder calls.*

At 18:00 London time, Sunday to Thursday, ask the booking system for tomorrow's appointments and send them all to the SMS provider in one batch.

```json
{
  "name": "Appointment reminders",
  "cron": { "expression": "0 18 * * 0-4", "time_zone": "Europe/London" },
  "nodes": [
    { "id": "bookings", "name": "Tomorrow's bookings",
      "request": { "method": "GET", "endpoint": "https://bookings.example.com/api/appointments?day={{ format(now:Europe/London + days(1), \"YYYY-MM-DD\") }}", "credential_id": "YOUR_BOOKING_KEY_ID" } },
    { "id": "sms", "name": "Send SMS batch",
      "request": { "method": "POST", "endpoint": "https://sms.example.com/v1/messages/batch", "credential_id": "YOUR_SMS_KEY_ID",
        "body": "{ \"messages\": { \"{{#each $.appointments}}\": { \"to\": \"{{ $.patient.phone }}\", \"body\": \"Hi {{ $.patient.first_name }}, a reminder of your appointment tomorrow at {{ $.time }} with {{ $.dentist }}. Reply C to cancel.\" } } }" } }
  ]
}
```

On a day with no bookings, `{{#each}}` over the empty list sends `{ "messages": [] }`, so make sure the batch endpoint accepts it. For a single text, the built-in [`twilio_send_sms`](/integrations/twilio) step does it with your Twilio account.

## For AI agents

### Morning ticket triage

*A support team runs an AI agent that reads new tickets, tags them, and drafts replies. It works, but someone has to remember to start it.*

Start the agent every weekday at 06:00. The run takes minutes, longer than the 10-second timeout, so the agent's API queues it and returns `202` right away.

```json
{
  "name": "Morning support triage",
  "cron": { "expression": "0 6 * * 1-5", "time_zone": "America/Denver" },
  "nodes": [
    { "id": "start", "name": "Start agent run",
      "request": { "method": "POST", "endpoint": "https://agents.acme.com/api/runs", "credential_id": "YOUR_AGENT_KEY_ID",
        "body": "{ \"agent\": \"support-triage\", \"task\": \"Tag and draft replies for every ticket opened since {{ now:America/Denver - days(1) }}. Post a summary to #support.\" }" } }
  ]
}
```

### An agent that schedules its own follow-up

*A coding agent finishes a migration and needs to check the result tomorrow, after the conversation has ended.*

The agent calls `POST /alarms` itself with a one-time job that wakes it again. It fires once and switches itself off. See the [agent guide](/agents) for the rules it should follow.

```json
{
  "name": "acme-api: verify orders backfill",
  "run_at": "2026-11-02T09:00:00-08:00",
  "nodes": [
    { "id": "reopen", "name": "Re-open task",
      "request": { "method": "POST", "endpoint": "https://agents.acme.com/api/runs", "credential_id": "YOUR_AGENT_KEY_ID",
        "body": "{ \"agent\": \"backend\", \"task\": \"Check that the orders backfill finished: row counts match and the error log is clean.\" }" } }
  ]
}
```

## When it's not the right fit

| The work needs… | Do this instead |
| --- | --- |
| More than 10 seconds per request | Put it behind an endpoint that returns `202`, and schedule that endpoint |
| One request per item in a list | Send the list to a batch endpoint, as in [appointment reminders](#tomorrows-appointment-reminders) |
| Filtering, branching, or loops | Filter with the source API's query parameters (like Stripe's `status=past_due`), or do it in your own endpoint |
| Faster than once a minute | Use a long-running worker or a queue |
| Direct database access | Expose an endpoint, then schedule it |
