> For the complete documentation index, see [llms.txt](https://fleet.hackmap.win/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fleet.hackmap.win/concepts/primitives.md).

# Primitives

Beyond streams and the event bus, Fleet exposes four primitives for state that lives outside any single slot. All of them are thin, typed wrappers over the framework's backend (Redis today). Plugins never see Redis keys or commands.

## Pool

A bag of items that producers fill and consumers borrow. The most common use case is "small set of valuable, reusable artifacts the fleet shares" — clearance cookies, JWT tokens, account credentials, warmed sessions, fingerprint profiles.

Pools are non-destructive on read. A plugin can call `list()` to browse, or `claim(...)` to take an exclusive lease that auto-releases after a TTL. Two slots that race for the same item don't see duplicates: the first claimer wins, the second sees `None`.

```python
pool = ctx.pool(COOKIE_POOL)

# producer
await pool.put(
    payload=CfCookie(cookies={...}, user_agent="...", proxy_url="...", site="x"),
    tags=CfCookieTags(site="x", fresh=True),
    ttl_seconds=1500,
)

# consumer — exclusive claim with 20s lease, auto-released on exit
async with pool.where(site="x", fresh=True).claim(hold_seconds=20) as item:
    if item is None:
        await ctx.submit(HARVESTER, HarvestRequest(site="x"))
        continue
    # item.payload is fully typed (CfCookie); item.tags is CfCookieTags
    response = await client.get(url, cookies=item.payload.cookies)
    if response.status_code == 403:
        await item.mark_bad("clearance rejected")
```

`where(**kwargs)` validates kwargs against the tags Pydantic model. Pass a field that doesn't exist and you get a `TypeError` at the call site, not at runtime. Pass a value of the wrong type and you get a `ValueError`.

`mark_bad(reason)` evicts the item from the pool permanently. Useful when a consumer discovers an item is unusable (expired session, rate-limited IP, dead account).

Pool entries are bounded by per-item TTL set at `put` time. The framework prunes expired entries opportunistically on every `put`. There's no global cap — if you need one, enforce it by checking `pool.size()` before producing.

## KV

Single named value with optional TTL. For state that's smaller than a pool and doesn't need querying. Examples: "last successful harvest timestamp for site X", "current best proxy for account Y", "version of the schema we deployed last".

```python
kv = ctx.kv("warmup")
await kv.set("last-harvest", time.time(), ttl_seconds=300)
ts = await kv.get("last-harvest")          # plain value or None

# pydantic round-trip
await kv.set("active-config", MyConfig(...), ttl_seconds=None)
cfg = await kv.get_as("active-config", MyConfig)
```

KV is namespaced. `ctx.kv("worker-state").get("foo")` and `ctx.kv("global-state").get("foo")` are different keys.

## Lock

Exclusive critical section across the whole fleet. For "only one slot anywhere should do this right now": de-duplicating harvest requests, serializing a migration step, gating an external API call that doesn't tolerate concurrency.

```python
async with ctx.lock("harvest:nowsecure.nl", hold_seconds=30, wait_seconds=2) as got_it:
    if not got_it:
        return  # someone else has it; we're done
    await do_the_thing()
# auto-released on context exit; also expires after hold_seconds if a slot crashes
```

`hold_seconds` is the maximum lease time. If your slot crashes inside the critical section, the lock auto-releases after this TTL — which is why you should set it slightly larger than your expected work duration, not larger than you can afford to wait if a worker dies.

`wait_seconds` is how long the caller blocks trying to acquire. Zero means "fail immediately if held"; a positive value polls with exponential backoff up to that deadline.

The implementation uses a token-keyed `SET NX EX` with a Lua-based release that only deletes if the token matches. A slot can't accidentally release a lock that's already expired and been re-acquired by another slot.

## Counter

Atomic integer shared across the fleet. Useful for "in-flight request count", "tokens consumed this hour", "active sessions per account". Different from `ctx.metrics`, which is per-slot.

```python
inflight = ctx.counter("inflight-requests")
await inflight.incr()
try:
    await do_request()
finally:
    await inflight.decr()

total = await inflight.get()    # current value, atomic
```

Increments are atomic via `INCRBY`. Concurrent increments from 10 slots calling `incr()` 100 times each end at exactly 1000. The framework guarantees no lost updates.

## Submit

Not a primitive in the same sense, but it belongs in the same conversation. `ctx.submit(QUEUE, payload)` enqueues a typed task onto another `BatchAutomation`'s queue. Payload is validated against the queue's declared model before transmission.

```python
await ctx.submit(HARVESTER, HarvestRequest(site=ctx.config.site))
```

The submission goes via Redis (`RPUSH fleet:queue:<name>`). For external submitters (a UI, a scheduled job, a CLI), the same effect is available through `POST /api/v1/automations/<type>/tasks`, which validates against the receiver's `TaskPayload` schema and writes onto the same queue.

## What's anonymous, what's named

Pools, queues, and streams are named contracts. Two automations agree on the name and the schema, the framework wires them up. Names live in shared constants (typically a contracts package).

KV, locks, and counters are anonymous primitives. The names are just strings — there's no `KV("name")` typed reference because there's no schema to share. You pick names by convention and document them in the consumer's code.

A reasonable convention is `<automation-type>:<purpose>` for anonymous names: `cf-cookie-harvester:last-run`, `cf-scraper:account-lock-12345`. Keeps the namespace flat and grepable.

## What's missing on purpose

There are a few primitives we didn't build because they're load-bearing for systems Fleet doesn't try to be:

No distributed transactions across multiple primitives. If you need "put into pool A and increment counter B atomically", you either use Redis MULTI/EXEC yourself (bypass the framework) or you accept best-effort consistency.

No subscription on pool changes. Consumers poll. If you want push notification when a producer adds an item, publish an event on the event bus alongside the put.

No rate-limit primitive. A counter plus your own logic gives you a token bucket, but the framework doesn't ship one. Reaching for `pool.size()` or `counter.get()` to make a flow decision is sometimes the right thing; sometimes it indicates you want a proper rate limiter and Fleet isn't the place to add one.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://fleet.hackmap.win/concepts/primitives.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
