Actions & Scheduling
Everything in CallMeLater is an action -- something scheduled to happen in the future. Actions operate in one of two modes: webhook or approval.
Webhook mode
Webhook mode is the default. A webhook action delivers an HTTP request to your endpoint at a scheduled time.
{
"schedule": { "preset": "tomorrow" },
"request": {
"method": "POST",
"url": "https://api.example.com/webhook",
"headers": { "X-Custom-Header": "value" },
"body": { "event": "trial_expired", "user_id": 42 }
}
}
Use webhook mode for trial expirations, delayed notifications, scheduled API calls, cleanup tasks, and follow-up triggers.
Approval mode
An approval action sends an interactive message to one or more recipients and collects their response (confirm, decline, or snooze).
{
"mode": "approval",
"schedule": { "wait": "2h" },
"gate": {
"message": "Please confirm the deployment",
"recipients": ["ops@example.com", "+1234567890"],
"confirmation_mode": "first_response"
},
"callback_url": "https://api.example.com/webhooks/response"
}
Approvals do not automatically execute anything. When someone responds, CallMeLater sends the response to your callback_url. Your system decides what to do next.
Use approval mode for approval workflows, human confirmations, check-ins, and escalation chains.
Scheduling
Every action needs a schedule that defines when it should fire. There are three ways to set it.
Presets
Named time references, resolved relative to now (or to a timezone if provided):
| Preset | When |
|---|---|
tomorrow | Tomorrow at the current time |
next_monday | Next Monday at the current time |
next_tuesday | Next Tuesday at the current time |
next_wednesday | Next Wednesday at the current time |
next_thursday | Next Thursday at the current time |
next_friday | Next Friday at the current time |
next_saturday | Next Saturday at the current time |
next_sunday | Next Sunday at the current time |
next_week | Next Monday at the current time |
1h, 2h, 4h | 1, 2, or 4 hours from now |
1d, 3d | 1 or 3 days from now |
1w | 1 week from now |
{
"schedule": {
"preset": "next_monday",
"timezone": "America/New_York"
}
}
Relative wait
A duration from now using schedule.wait:
{ "schedule": { "wait": "30m" } }
| Format | Meaning | Example |
|---|---|---|
Ns | N seconds | 30s = 30 seconds |
Nm | N minutes | 5m = 5 minutes |
Nh | N hours | 2h = 2 hours |
Nd | N days | 1d = 1 day |
Nw | N weeks | 1w = 1 week |
NM | N months | 1M = 1 month |
Exact time
An ISO 8601 UTC timestamp using scheduled_for:
{
"scheduled_for": "2026-04-01T14:30:00Z"
}
Timezone
Optional. Defaults to UTC. Provide a timezone when using presets so that times like tomorrow and next_monday resolve to the correct local time.
{
"schedule": {
"preset": "tomorrow",
"timezone": "Europe/Paris"
}
}
Lifecycle states
Actions move through a series of states from creation to completion.
Webhook mode:
scheduled → resolved → executing → executed
↘ failed
Approval mode:
scheduled → resolved → executing → awaiting_response → executed
↘ failed
↘ expired
Any non-terminal action can also be cancelled.
| State | Description | Next states |
|---|---|---|
scheduled | Schedule is being resolved to an exact timestamp | resolved, cancelled |
resolved | Waiting for the scheduled time to arrive | executing, cancelled |
executing | HTTP request or reminder is being delivered | executed, failed, awaiting_response, resolved (retry) |
awaiting_response | Reminder sent, waiting for a human reply | executed, failed, expired, cancelled, scheduled (snooze) |
executed | Completed successfully (terminal) | -- |
failed | Failed permanently (terminal) | resolved (manual retry) |
expired | Approval timed out without response (terminal) | -- |
cancelled | Cancelled before completion (terminal) | -- |
Recurring actions
Any action can repeat on a schedule by adding a recurrence configuration. After each successful execution, the action automatically re-schedules itself for the next occurrence.
{
"schedule": { "wait": "5m" },
"request": { "url": "https://api.example.com/health-check" },
"recurrence": {
"frequency": 1,
"unit": "h",
"end_type": "count",
"max_occurrences": 24
}
}
How it works
- The action executes normally at the scheduled time
- On success, it re-schedules itself by the configured interval
- The
recurrence_countincrements after each execution - When the end condition is met, the action reaches
executed(terminal) - Cancelling the action at any point stops all future occurrences
Lifecycle
A recurring action cycles through resolved → executing → resolved until the end condition is met:
resolved → executing → resolved → executing → resolved → ... → executed
If a recurring action fails, it stays in failed status and does not re-schedule. You can manually retry to resume the cycle.
End conditions
end_type | Behavior |
|---|---|
never | Repeats indefinitely until cancelled |
count | Stops after max_occurrences total executions |
date | Stops when the next scheduled time would be after end_date |
Units
| Unit | Meaning |
|---|---|
m | Minutes (minimum: 5) |
h | Hours |
d | Days |
w | Weeks |
M | Months |
Recurring approvals
Approval-mode actions can also recur. Each occurrence resets the gate -- recipients must respond again for each cycle.
Idempotency keys
Use idempotency_key to prevent duplicate actions when your system retries requests or a user double-clicks.
{
"idempotency_key": "trial-end-user-42",
"schedule": { "wait": "14d" },
"request": { "url": "https://api.example.com/expire" }
}
Key format patterns:
trial:user:123
deploy:v2.1
invoice:1234:reminder
weekly-report:2026-W07
Keys are scoped per account, up to 255 characters, and case-sensitive.
Behavior when a matching key already exists:
If an action with the same idempotency key already exists (in any status), the request is rejected with a 422 validation error. Idempotency keys are permanent and cannot be reused, even after the original action reaches a terminal state.
To schedule a new action that replaces an existing one, use dedup keys with coordination.on_create: "replace_existing" instead.
Dedup keys
Dedup keys group related actions together and control how they interact. Unlike idempotency keys (which prevent duplicates of the same request), dedup keys coordinate behavior across different actions that share a logical group.
{
"dedup_keys": ["deploy:api-service"],
"coordination": {
"on_create": "replace_existing"
},
"schedule": { "wait": "1h" },
"request": { "url": "https://ci.example.com/deploy" }
}
on_create behaviors
Controls what happens when you create a new action with the same dedup key as an existing non-terminal action.
| Behavior | Description |
|---|---|
skip_if_exists | Return the existing action instead of creating a new one (response includes meta.skipped: true) |
replace_existing | Cancel all existing non-terminal actions with matching keys, then create the new action |
on_execute behaviors
Controls what happens at execution time based on other actions sharing the same dedup key. This is a nested object with condition-based logic:
{
"dedup_keys": ["deploy:api-service"],
"coordination": {
"on_execute": {
"condition": "skip_if_previous_pending",
"on_condition_not_met": "cancel",
"reschedule_delay": 300,
"max_reschedules": 10
}
}
}
| Field | Description |
|---|---|
condition | skip_if_previous_pending, execute_if_previous_failed, execute_if_previous_succeeded, or wait_for_previous |
on_condition_not_met | cancel, reschedule, or fail |
reschedule_delay | Seconds to wait before retrying when rescheduled (used with reschedule) |
max_reschedules | Maximum number of reschedule attempts (used with reschedule) |
Format rules
- Alphanumeric characters plus
_,:,.,- - No spaces, slashes, or special characters
- Case-sensitive (
Deploy:APIanddeploy:apiare different keys) - Maximum 10 keys per action
- Scoped to your account
Valid: deploy:production, user:42:notifications, workflow.onboarding.step-1
Invalid: key with spaces, key/with/slashes, key@email.com