Run proactive work durably with reviewable commitments
How to study this lesson
Read one section, trace the worked example, and try the knowledge check. Bookmark saves a shortcut; notes appear in My notebook. Mark lesson complete is your own assessment and does not mark its lab passed.
By the end, you can
- Define scheduling overlap and catch-up behavior.
- Persist proposals, approvals, and operation identity.
- Recover interrupted automation without duplicate effects.
A schedule needs more than a timestamp
A recurring agent task has a cadence, timezone, input scope, and policy for what happens when the previous run is still active. It also needs behavior after downtime. Should missed runs be skipped, combined, or replayed? These choices affect user experience and resource consumption. A daily digest usually should not send thirty separate historical messages when a worker returns after a month, unless the user specifically wants that behavior.
Temporal's Schedule documentation makes overlap and catch-up policies explicit, including skipping, buffering, allowing concurrent runs, and other choices. [Temporal Schedule documentation](https://docs.temporal.io/schedule). The important design habit is to specify these policies even if you use a different scheduler. A timestamp answers when work becomes eligible; it does not define concurrency, recovery, or whether an old result is still useful to the user.
Persist the decision before awaiting a person
A proactive agent can prepare a proposal and then wait for review without holding a live process open. Store proposal ID, task ID, exact target, payload, payload fingerprint, source evidence, creation time, expiration, and policy version. A human response references this concrete record. If the agent later changes the payload, it creates a new revision and cannot silently reuse approval for the older content.
Suppose an anomaly report proposes notifying team A about build 17. Before approval arrives, the incident is resolved and the proposed recipient changes to team B. The approval handler should compare the reviewed fingerprint and current state, then either reject the stale request or present a revised proposal. Merely receiving a button click is not enough; the server must know what was reviewed and whether that action remains relevant and authorized.
Keep the operation ledger separate from the inbox
The inbox tracks which incoming events were admitted. The operation ledger tracks which outgoing effects were intended, attempted, and observed. A hundred input events might produce one digest, while one event might produce two separately authorized operations. Use logical operation identifiers based on the task and proposal revision rather than each network attempt. Bind the payload fingerprint to the identifier to detect accidental reuse with changed content.
An outbox can atomically record the intent to deliver alongside the application's state update. A separate dispatcher performs the delivery and records the result. This avoids losing intent between local transactions, but it does not by itself guarantee exactly-once effects at an external service. The sink must honor idempotency or expose reconciliation evidence. Keep uncertain delivery status visible until it is resolved; a local sent flag written before the remote call is not proof of delivery.
Revalidate before committing a delayed effect
Conditions can change while a proposal waits in a queue. Check freshness, ownership, destination, permissions, and the approved payload immediately before the effect. For example, a document may have been deleted, a user may have revoked authorization, or the same incident may already have been handled manually. The correct response can be to mark the proposal obsolete, not to execute it merely because the schedule once considered it eligible.
Avoid turning revalidation into a reason to ask for unnecessary permission repeatedly. Persist the user's standing authorization with its scope and apply it when it covers the current action. Ask only when the new action falls outside that scope or the reviewed proposal changed materially. The execution layer should make this distinction from explicit policy facts. That lets a proactive system remain useful without weakening the connection between authorization and the operation that actually occurs.
Test time and crashes as ordinary inputs
Use a supplied clock in simulations and tests rather than calling the wall clock throughout business logic. Replay a missed schedule, overlapping runs, a stale approval, duplicate event delivery, and a crash after the sink accepted the effect. Assert the number of logical operations and the final proposal states. A test suite should make it possible to explain every notification or modification in a trace using the event, trigger, proposal, and operation records.
The lesson code binds an approval to a canonical payload fingerprint, rejects a changed proposal, and deduplicates a simulated sink. It is intentionally in memory and has no authentication or durable storage. The mini project combines this idea with event windows and emits proposals rather than sending notifications. Extending it into production requires persistent transactions and real integration contracts, while preserving the same inspectable sequence from observed evidence to authorized commitment.
Work through the code
This teaching simulation hashes a canonical local payload and uses an in-memory sink to show stale-approval rejection and operation deduplication. It is not a digital signature, authenticated approval service, durable outbox, or external messaging integration.
import hashlib
import json
def fingerprint(payload):
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
return hashlib.sha256(canonical.encode()).hexdigest()
approved_payload = {'target': 'team-A', 'message': 'Review build 17'}
approval = fingerprint(approved_payload)
sink = {}
def commit(operation, payload, reviewed_hash):
current_hash = fingerprint(payload)
if current_hash != reviewed_hash:
return 'stale-approval'
if operation in sink and sink[operation] != current_hash:
return 'operation-conflict'
sink.setdefault(operation, current_hash)
return 'recorded'
changed = dict(approved_payload, target='team-B')
print('changed:', commit('op17', changed, approval))
print('original:', commit('op17', approved_payload, approval))
print('retry:', commit('op17', approved_payload, approval))
print('logical effects:', len(sink))
changed: stale-approval original: recorded retry: recorded logical effects: 1
Run Python snippets locally with the prerequisites named above. The module coding lab runs directly in your browser.
Pause and reason
A weekly summary is eligible during a three-week outage. The user wanted a current weekly overview, not historical messages. Propose a catch-up policy and a record to retain.
Check your understanding
What does an outbox transaction guarantee by itself?
Your notes
Explain the mechanism in your own words. Add a failure you want to test.