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:

bash
cd packages/gateway && pnpm dev
# Should show: ZeroLock Gateway running at http://localhost:8080

Example 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:

plaintext
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:

plaintext
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

bash
cd examples/quickstart/01-hello-zerolock
python main.py

What You'll See

plaintext
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: ✓ CORRECT

The Code

python
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...")
                continue

Key Takeaways

WhatWhy
Always read the versionYou need it to detect conflicts
Handle StaleCommitErrorRetry 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:

plaintext
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:

  1. Write: Worker writes "I want this task"
  2. Wait: Brief sync ensures all claims are visible
  3. Read: Worker checks who actually got the task
  4. Result: Only one worker sees themselves as the owner
plaintext
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

bash
cd examples/quickstart/02-task-queue
python main.py

What You'll See

plaintext
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: ✓ CORRECT

The Code

python
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

plaintext
┌─────────┐         ┌──────────┐         ┌───────────┐
│ pending │──claim──►│ claimed  │──done───►│ completed │
└─────────┘         │ by: W1   │         └───────────┘
                    └──────────┘
                         │
                         │ release (if worker fails)
                         ▼
                    ┌─────────┐
                    │ pending │  (back to queue)
                    └─────────┘

Key Functions

FunctionWhat 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

WhatWhy
Always check result.claimedYou might have lost the race
Move on if you loseDon't retry the same task, try the next one
Call complete_task() when doneOtherwise 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:

plaintext
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:

plaintext
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

bash
cd examples/quickstart/03-shared-notepad
python main.py

What You'll See

plaintext
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 PRESERVED

The Code

python
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

TypeMethodWhen to Use
Decisioncommit_shard()Single values: counters, configs, status
Additiveappend_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

WhatWhy
Use type="additive"Enables CRDT append
No version neededAppends never conflict
Order isn't guaranteedBut all data is preserved

What You've Learned

You now know the three core patterns:

PatternProblemSolutionMethod
OCCLost updatesVersion checking + retrycommit_shard()
Task ClaimingDuplicate workWrite-wait-rereadclaim_task()
CRDT AppendLost appendsConflict-free mergeappend_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: