Core Concepts
This page explains the foundational ideas behind Verity: how effects, leases, and fence tokens work together to guarantee exactly-once execution for real-world actions.
Effects
An effect is a single, protected unit of work — a real-world action that Verity tracks from initiation to completion. Examples:
- Charging a credit card
- Issuing a refund
- Provisioning a virtual machine
- Sending a notification email
- Updating a CRM record
Each effect is identified by a unique effect key — a string you choose that represents this specific action instance. Good effect keys are business-meaningful:
"refund:order_48392" // a refund for a specific order
"provision-vm:tenant_abc" // VM provisioning for a tenant
"welcome-email:user_42" // welcome email for a specific user
"crm-update:deal_9001" // CRM update for a dealEvery effect goes through a lifecycle:
IDLE → RUNNING → COMMITTED (success)
→ FAILED (error recorded)
→ EXPIRED (lease timed out — recovery needed)protect() with the same effect key always returns the same result after the first successful commit. This is how Verity prevents duplicate actions.Leases
A lease is a time-limited, exclusive lock on an effect. When your agent calls protect(), Verity first acquires a lease, giving your agent sole ownership of the action for a specified duration (default: 30 seconds).
Key properties of leases:
- Exclusive — only one agent can hold the lease at a time. Others receive a 409 Conflict.
- Time-limited — leases expire after their duration. This prevents dead agents from blocking work forever.
- Auto-renewable — the SDK automatically renews leases in the background while your
act()function runs, preventing expiry during long operations. - Configurable — duration ranges from 5s to 120s (default 30s). Renewal starts at 65% of the lease duration.
Lease States
When you request a lease, the response tells you the current state:
| Status | Meaning | SDK Behavior |
|---|---|---|
granted | You got the lease. Effect is now RUNNING. | Proceeds to observe/act |
cached_completed | Effect already committed by a prior attempt. | Returns cached result immediately |
cached_failed | Effect previously failed. Admin reset required. | Throws EffectPreviouslyFailedError |
Prior State
When a lease is granted, the priorState field tells you what happened before:
| Prior State | Meaning | Observe Behavior |
|---|---|---|
none | Brand-new effect. No prior attempt. | Skip observe → go to act() |
expired | Prior agent held this lease but it expired. Unknown if action completed. | Call observe() to check external system |
reset | Admin explicitly reset a failed effect. | Skip observe → go to act() (prior attempt definitively failed) |
Fence Tokens
A fence token is a monotonically increasing integer assigned to each lease acquisition for an effect. It's the core mechanism that prevents "zombie" agents from causing harm.
Here's the scenario fence tokens prevent:
- Agent A acquires lease for
refund:order_123→ fence token = 1 - Agent A's network stalls. Lease expires.
- Agent B acquires lease for same effect → fence token = 2
- Agent B processes the refund and commits with fence token 2
- Agent A's network recovers. It tries to commit with fence token 1.
- Verity rejects the commit — fence token 1 is stale. Agent A is a zombie.
Without fence tokens, Agent A's stale commit could overwrite Agent B's valid result, or worse, trigger a duplicate action. Fence tokens are Verity's equivalent of distributed locking with fencing — a well-established pattern in distributed systems.
The Observe / Act Pattern
The observe and act callbacks are the heart of Verity's crash recovery mechanism.
act() — Execute the Action
This is your real-world side effect. It runs when the effect hasn't been performed yet. It must return the result of the action (which Verity commits to the ledger).
act: async () => {
return await stripe.refunds.create({
charge: chargeId,
amount: 4999,
});
}observe() — Check Before Re-Acting
This is called only when priorState === 'expired' — meaning a previous agent held this lease, the lease expired, and Verity doesn't know if the action completed.
Your observe function checks the external system:
- Return the result if the action already happened (Verity commits it)
- Return
nullorundefinedto fall through toact()
observe: async () => {
// Check Stripe for an existing refund
const existing = await stripe.refunds.list({
charge: chargeId,
limit: 1,
});
return existing.data.length > 0 ? existing.data[0] : null;
}priorState: 'none'— brand-new effect, nothing to observepriorState: 'reset'— admin reset a failure, prior attempt definitively failed, no need to check
Decision Flow
protect() called
│
▼
Request Lease
│
├── cached_completed → return cached result
├── cached_failed → throw EffectPreviouslyFailedError
│
└── granted
│
├── priorState: 'none' → act()
├── priorState: 'reset' → act()
└── priorState: 'expired' → observe()
├── found result → commit observed result
└── null → act()
│
▼
commit resultNamespaces
Namespaces are logical groupings for effects. They serve two purposes:
- Organization — group related effects (e.g.,
payments,notifications,provisioning) - Access control — namespaces can be frozen by admins to block all new actions during incidents
Set a default namespace when initializing the client, or override it per-call:
const verity = new VerityClient({
baseUrl: '...',
apiKey: '...',
namespace: 'payments', // default for all protect() calls
});
// Override for a specific call
await verity.protect('send-receipt:order_123', opts, {
namespace: 'notifications',
});Exactly-Once vs Idempotency Keys
You might be thinking: "Can't I just use idempotency keys?" Here's the difference:
| Capability | Idempotency Keys | Verity |
|---|---|---|
| Duplicate prevention | Single provider (e.g., Stripe) | Any external system |
| Crash recovery | None — you re-send blindly | Observe pattern checks first |
| Zombie protection | None | Fence tokens block stale agents |
| Audit trail | Manual logging | Automatic, structured, queryable |
| Multi-step workflows | DIY per-step logic | Workflow builder with case + run tracking |
| Concurrent agent safety | Not handled | Leases + fence tokens |
Idempotency keys are great when the external system supports them natively (like Stripe). Verity is for everything else — and for coordinating across systems.
Environments (Test / Live)
Verity API keys are prefixed with their environment:
vt_test_— isolated test environment, safe for developmentvt_live_— production environment
Data between environments is completely separated via PostgreSQL row-level security. A test API key cannot see or modify live effects, and vice versa. This is enforced at the database level, not just the application layer.
What's Next?
- TypeScript SDK Reference — full API documentation with all options
- Python SDK Reference — identical API, Pythonic conventions
- Workflows — multi-effect orchestration with cases and runs
- Error Handling — every error type explained