Quickstart: Your First ZeroLock Agents
These three examples teach the fundamental patterns you'll use in every ZeroLock application. Complete them in order—each builds on the previous.
Prerequisites
Make sure the ZeroLock gateway is running:
cd packages/gateway && pnpm dev
# Should show: ZeroLock Gateway running at http://localhost:8080Example 1: Hello ZeroLock
Time: 5 minutes
Location: examples/quickstart/01-hello-zerolock/
What You'll Learn
How to prevent lost updates when multiple agents modify the same data.
The Problem
Two agents try to increment a counter. Each reads 0, adds 1, and writes 1. But both read before either writes—so the counter ends at 1 instead of 2:
Agent A reads: 0 Counter: 0
Agent B reads: 0 Counter: 0
Agent A writes: 1 Counter: 1
Agent B writes: 1 Counter: 1 ← Should be 2!This is called a lost update. Agent A's increment is gone forever.
The Solution
ZeroLock tracks a version number with each piece of data. When you save, you include the version you read. If someone else saved first (changing the version), your save fails and you retry with fresh data:
Agent A reads: 0, version=1 Counter: 0 (v1)
Agent B reads: 0, version=1 Counter: 0 (v1)
Agent A saves: 1, expecting v1 ✓ Counter: 1 (v2)
Agent B saves: 1, expecting v1 ✗ "Version mismatch!"
Agent B reads: 1, version=2 Counter: 1 (v2)
Agent B saves: 2, expecting v2 ✓ Counter: 2 (v3)Both increments are preserved!
Run It
cd examples/quickstart/01-hello-zerolock
python main.pyWhat You'll See
WITHOUT ZEROLOCK (NAIVE)
Final counter value: 6
Expected value: 10
Lost updates: 4
Result: ✗ DATA LOSS
WITH ZEROLOCK (OCC)
[Agent-A] Incremented 0 → 1
[Agent-B] Conflict detected, retrying...
[Agent-B] Incremented 1 → 2
...
Final counter value: 10
Expected value: 10
Result: ✓ CORRECTThe Code
from zerolock_sdk import ZeroLockClient, StaleCommitError
async def increment_counter(agent_id: str):
async with ZeroLockClient("http://localhost:8080", agent_id=agent_id) as client:
while True:
# Step 1: Read the current value AND its version
shard = await client.get_shard("my-counter")
current_value = shard.value
version = shard.version
# Step 2: Calculate the new value
new_value = current_value + 1
# Step 3: Try to save with version check
try:
await client.commit_shard("my-counter", new_value, version)
print(f"[{agent_id}] Incremented {current_value} → {new_value}")
break # Success!
except StaleCommitError:
# Someone else saved first — retry with fresh data
print(f"[{agent_id}] Conflict, retrying...")
continueKey Takeaways
| What | Why |
|---|---|
| Always read the version | You need it to detect conflicts |
Handle StaleCommitError | Retry with fresh data when conflicts happen |
Use commit_shard() | This is how you save with version checking |
This pattern is called Optimistic Concurrency Control (OCC)—assume there's no conflict, detect it if there is.
Example 2: Task Queue
Time: 10 minutes
Location: examples/quickstart/02-task-queue/
What You'll Learn
How to ensure each task is processed exactly once when multiple workers compete.
The Problem
Three workers check a queue for tasks. All three see task-1 and start processing it:
Worker 1 sees: [task-1, task-2, task-3]
Worker 2 sees: [task-1, task-2, task-3]
Worker 3 sees: [task-1, task-2, task-3]
All three grab task-1!
Worker 1 processes task-1
Worker 2 processes task-1 ← DUPLICATE!
Worker 3 processes task-1 ← DUPLICATE!You've wasted time and resources doing the same work three times.
The Solution
ZeroLock's task claiming ensures only one worker wins each task:
- Write: Worker writes "I want this task"
- Wait: Brief sync ensures all claims are visible
- Read: Worker checks who actually got the task
- Result: Only one worker sees themselves as the owner
Worker 1 claims task-1
Worker 2 claims task-1
Worker 3 claims task-1
After sync:
Worker 1 reads owner: Worker-2 → "I lost, try task-2"
Worker 2 reads owner: Worker-2 → "I won!"
Worker 3 reads owner: Worker-2 → "I lost, try task-2"Run It
cd examples/quickstart/02-task-queue
python main.pyWhat You'll See
WITHOUT ZEROLOCK (NAIVE)
[worker-1] Processed task-1: 10² = 100
[worker-2] Processed task-1: 10² = 100 ← DUPLICATE!
Duplicate processing: 3
Result: ✗ DUPLICATE WORK
WITH ZEROLOCK
[worker-1] Claimed task-1
[worker-2] Lost race for task-1 to worker-1
[worker-2] Claimed task-2
[worker-3] Claimed task-3
Duplicate processing: 0
Result: ✓ CORRECTThe Code
from zerolock_sdk import ZeroLockClient
from zerolock_sdk.task_claim import claim_task, complete_task, get_pending_tasks
async def worker(worker_id: str):
async with ZeroLockClient("http://localhost:8080", agent_id=worker_id) as client:
while True:
# Step 1: See what tasks are available
pending = await get_pending_tasks(client, "task-queue")
if not pending:
break # No more tasks
# Step 2: Try to claim one
task = pending[0]
result = await claim_task(client, "task-queue", task.id, worker_id)
if not result.claimed:
# Another worker got it first — try the next task
print(f"[{worker_id}] Lost race for {task.id}")
continue
# Step 3: Process the task (we own it now)
print(f"[{worker_id}] Processing {task.id}")
answer = task.payload["number"] ** 2
# Step 4: Mark it complete
await complete_task(client, "task-queue", task.id, worker_id)
print(f"[{worker_id}] Completed {task.id}: {answer}")Task Lifecycle
┌─────────┐ ┌──────────┐ ┌───────────┐
│ pending │──claim──►│ claimed │──done───►│ completed │
└─────────┘ │ by: W1 │ └───────────┘
└──────────┘
│
│ release (if worker fails)
▼
┌─────────┐
│ pending │ (back to queue)
└─────────┘Key Functions
| Function | What It Does |
|---|---|
get_pending_tasks() | List tasks that haven't been claimed |
claim_task() | Try to claim a task (returns claimed=True/False) |
complete_task() | Mark a task as finished |
release_task() | Return a task to the queue (if your worker fails) |
Key Takeaways
| What | Why |
|---|---|
Always check result.claimed | You might have lost the race |
| Move on if you lose | Don't retry the same task, try the next one |
Call complete_task() when done | Otherwise the task stays "claimed" forever |
Example 3: Shared Notepad
Time: 5 minutes
Location: examples/quickstart/03-shared-notepad/
What You'll Learn
How to append data without conflicts using CRDTs.
The Problem
Three writers add notes to a shared notepad. But with read-modify-write, concurrent appends overwrite each other:
Alice reads: []
Bob reads: []
Alice writes: ["Buy groceries"]
Bob writes: ["Team meeting"] ← Overwrote Alice's note!
Final notepad: ["Team meeting"]
Missing: ["Buy groceries"]The Solution
ZeroLock's additive shards use CRDTs (Conflict-free Replicated Data Types). Instead of "read, modify, write," you just say "append this." The system merges concurrent appends automatically:
Alice appends: "Buy groceries"
Bob appends: "Team meeting"
(both at same time)
Final notepad: ["Buy groceries", "Team meeting"]Order might vary, but nothing is lost.
Run It
cd examples/quickstart/03-shared-notepad
python main.pyWhat You'll See
WITHOUT ZEROLOCK (NAIVE)
Notes in notepad: 4
Expected notes: 8
Lost notes: 4
Result: ✗ NOTES LOST
WITH ZEROLOCK (CRDT APPEND)
[Alice] Added: Buy groceries
[Bob] Added: Team meeting at 3pm
[Charlie] Added: Gym at 6am
...
Notes in notepad: 8
Lost notes: 0
Result: ✓ ALL PRESERVEDThe Code
from zerolock_sdk import ZeroLockClient
from zerolock_sdk.types import CreateShardOptions
async def setup():
async with ZeroLockClient("http://localhost:8080", agent_id="setup") as client:
# Create an ADDITIVE shard (not decision)
await client.create_shard("notepad", CreateShardOptions(
type="additive", # ← This enables CRDT
initial_value=[]
))
async def writer(writer_id: str, notes: list[str]):
async with ZeroLockClient("http://localhost:8080", agent_id=writer_id) as client:
for note in notes:
# Just append — no version needed, no conflicts possible
await client.append_item("notepad", {
"author": writer_id,
"content": note,
})
print(f"[{writer_id}] Added: {note}")Decision Shards vs Additive Shards
| Type | Method | When to Use |
|---|---|---|
| Decision | commit_shard() | Single values: counters, configs, status |
| Additive | append_item() | Collections: logs, findings, messages |
When to Use Additive Shards
- Research findings: Multiple agents gathering data
- Code review comments: Reviewers adding notes
- Logs and audit trails: Events from many sources
- Task queues: Combined with claiming
Key Takeaways
| What | Why |
|---|---|
Use type="additive" | Enables CRDT append |
| No version needed | Appends never conflict |
| Order isn't guaranteed | But all data is preserved |
What You've Learned
You now know the three core patterns:
| Pattern | Problem | Solution | Method |
|---|---|---|---|
| OCC | Lost updates | Version checking + retry | commit_shard() |
| Task Claiming | Duplicate work | Write-wait-reread | claim_task() |
| CRDT Append | Lost appends | Conflict-free merge | append_item() |
These patterns combine to handle any coordination challenge. Most real-world ZeroLock applications use all three.
Next Steps
Ready to add AI? Continue with:
- Framework Examples — LangGraph, CrewAI, AutoGen, LlamaIndex
- Use Case Examples — Real-world AI agent scenarios
- Advanced Patterns — Deep dives into each pattern
