Scheduling AI Workflows Without Duplicate Actions
Run scheduled AI workflows that never act twice: separate schedules from runs, checkpoint state, use idempotency keys, classify retries, reconcile uncertain outcomes and test failures.
Contents
- Separate a schedule from an execution
- Design the execution state machine
- Make duplicate delivery safe
- Retry the right failures
- Reconcile uncertain outcomes
- Handle approval expiry and cancellation
- Inject failures around the side effect
- Operate the schedule over time
- Frequently asked questions
- Why can a scheduled job run twice?
- Are retries always safe?
- What happens if the tool succeeds but the process crashes?
- Can a cancelled job undo an action it already completed?
Image: William Dmytrow on Unsplash.
A scheduled AI workflow is safe when running it twice, or stopping it halfway, cannot repeat an external action. Separate the schedule from each run, record progress in durable state, give every external action an idempotency key, and when a call's outcome is uncertain, check the world before retrying.
Here is the failure that matters.
The workflow sends the weekly update.
The provider accepts it.
The process crashes before recording success.
On restart, a naive retry sends it again.
Starting a job on time is easy. Knowing whether it already happened is the hard part. This guide is part of the AI harness engineering guide.
Separate a schedule from an execution
Keep three identities distinct:
| Thing | Example | Identity |
|---|---|---|
| Job | "Weekly team update, Mondays 09:00" | job_weekly_update |
| Run | The run for Monday 20 October | job_weekly_update@2026-10-20 |
| Attempt | The second try at that run | job_weekly_update@2026-10-20#2 |
The run identity comes from the job and its scheduled time, not from when it happened to start. Two deliveries of the same trigger then share one run ID, which makes duplicates detectable.
Decide in advance:
- Time zone. Store schedules with an explicit zone and test daylight-saving changes.
- Missed runs. If the machine was off at 09:00, run late, skip, or ask?
- Overlap. If last week's run is still going, wait, skip or cancel it?
Design the execution state machine
Every run moves through a small set of states:
| State | Meaning |
|---|---|
pending |
Scheduled, not started |
running |
In progress, with a checkpoint |
awaiting_approval |
Paused for you to approve an exact action |
uncertain |
An external call may or may not have succeeded |
succeeded |
Final state verified |
failed |
Stopped with a recorded reason |
cancelled |
Stopped by you; completed effects listed |
Checkpoint what is needed to resume, not the whole conversation: completed steps, effect IDs, pending approvals. Anthropic's work on long-running agent harnesses makes the same point for coding agents, keeping a progress file and clean artifacts so each session can resume incremental work (Anthropic).
succeeded means verified, not claimed. If the run drafted an update, confirm the draft exists.
Make duplicate delivery safe
An idempotency key is a unique identifier for one intended effect. If the same key arrives twice, the receiver performs the effect once.
Build the key from what should be unique:
key = job_id + scheduled_time + step + target
= "job_weekly_update|2026-10-20T09:00+01:00|send|team-list"Then:
- If the downstream API supports idempotency keys, pass yours and let it deduplicate.
- If it does not, record the key and the resulting effect ID locally before reporting success, and check for an existing effect before acting.
- If neither is possible, treat the step as non-idempotent: never retry it automatically.
Be honest about scope. Your key prevents duplicates the system can see. It cannot make an external service that lacks deduplication behave exactly once.
Retry the right failures
Not every error deserves another try.
| Failure | Retry? |
|---|---|
| Network timeout before the request was sent | Yes, with backoff |
| Provider rate limit or temporary error | Yes, with backoff and a cap |
| Invalid input or schema error | No. Fix the input |
| Permission denied or approval missing | No. Stop and report |
| Timeout after the request may have been accepted | Not yet. Mark uncertain and reconcile |
| Budget exhausted | No. Stop visibly |
Cap attempts, total time and spend. Every final failure records its reason.
Reconcile uncertain outcomes
When an action's outcome is unknown, look before you leap:
- Query the downstream system for an effect matching the idempotency key, or the expected content and time window.
- If found, record the effect ID and mark the step done.
- If clearly absent, retry once under the same key.
- If you cannot tell, stop and ask a person.
Asking is a correct outcome. For a consequential, non-idempotent action, a duplicate is worse than a delay.
Handle approval expiry and cancellation
Approvals expire. If a run waits for approval and resumes the next day, check the approval again. Do not revive yesterday's consent. The permissions guide covers exact-action approval.
Cancellation stops new work. It cannot undo completed actions. When cancelled, list what already happened, with effect IDs, and what did not.
Inject failures around the side effect
Test the dangerous moments deliberately, in a sandbox:
| Test | Interrupt the process | Expected |
|---|---|---|
| Before the call | Just before sending | Resume sends once |
| During the call | While the request is in flight | Run becomes uncertain, reconciliation decides |
| After the call | After acceptance, before recording | Reconciliation finds the effect; no second send |
| Duplicate trigger | Deliver the same schedule event twice | One run, one effect |
| Restore | Restore the database from before the send | Reconciliation finds the effect; no second send |
The last row matters: restoring a backup rolls back your records, not the outside world. The backup and restore runbook builds on this.
Check final state in every test. Count the messages in the sandbox inbox; do not trust the log alone.
Operate the schedule over time
Watch a handful of signals:
runs stuck in running or uncertain,
jobs failing repeatedly,
runs skipped because of overlap,
approvals that expire unanswered.
Send these to a person rather than letting retries absorb them. The logging guide covers the receipts these signals come from, and the troubleshooting guide covers diagnosis.
Frequently asked questions
Why can a scheduled job run twice?
Schedulers, queues and restarts can deliver the same trigger more than once, and a crash can hide whether an action already happened.
Are retries always safe?
No. Retry only failures that are known not to have produced an effect, or effects protected by an idempotency key.
What happens if the tool succeeds but the process crashes?
The run becomes uncertain. On restart, reconcile by checking the downstream system before doing anything else.
Can a cancelled job undo an action it already completed?
No. Cancellation stops future steps. Completed external actions stay done and should be listed.