> 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/automations.md).

# Automations

An automation is a class that the framework instantiates inside each worker process. Two base classes are provided, and you pick the one that matches the shape of the work.

## ContinuousAutomation

For work that runs as a loop. Each slot calls `run_slot(ctx)` once; the slot is "done" when that coroutine returns. Inside, you typically loop until `ctx.shutdown` fires.

The CAPTCHA-solving case is the prototype: each browser slot opens a page, watches for tokens, and emits them. The loop doesn't stop until the worker shuts down or the slot is recycled.

```python
@register("captcha-solver")
class CaptchaSolver(ContinuousAutomation[CaptchaConfig]):
    Config = CaptchaConfig

    async def run_slot(self, ctx):
        async with open_browser(ctx) as browser:
            while not ctx.shutdown.is_set():
                token = await wait_for_token(browser)
                await ctx.emit(token)
```

## BatchAutomation

For work that comes in as discrete tasks. The framework pulls a task from a Redis BLPOP queue and calls `run_one(task, ctx)` once per task. The slot is a long-running worker that processes one task at a time; concurrency is the number of slots.

```python
@register("account-creator")
class AccountCreator(BatchAutomation[AccountConfig]):
    Config = AccountConfig

    async def run_one(self, task, ctx):
        account = await create_account(task.payload, ctx)
        await ctx.emit(account)
```

The task queue lives at `tasks:<type>:pending` in Redis. Push to it with an admin client or have another automation push to it. The task payload is opaque to the framework; the plugin decides the schema.

## Which one to use

Continuous is right when there's no natural "task" — the work is a steady-state producer. Captcha solving, scraping a feed, watching a websocket.

Batch is right when work is discrete and you want concurrency without overcomplicating things. Account creation, processing a queue of URLs, retrying failed jobs.

If you can't decide, lean batch. A batch automation with a single endless task fed back into its own queue can do anything a continuous one can. The reverse isn't quite true.

## Context

Whichever base class you pick, your callback receives a `Context` object. It owns the lifecycle of one slot and exposes everything the plugin should need.

```python
ctx.config              # validated Pydantic config (live; reflects current desired)
ctx.shutdown            # asyncio.Event set when the slot is being torn down
ctx.logger              # scoped logger ("automation_type.slot.N")
ctx.slot_id             # 0..N-1 within this worker
ctx.proxy_url           # eagerly resolved proxy URL for this slot (str | None)
ctx.proxy               # ProxyHandle for per-request rotation / country switching
ctx.browser             # BrowserResource if automation declared Browser = BrowserNeeds(...), else None

ctx.emit(payload)       # push a typed payload to this automation's own stream
ctx.stream(spec)        # read another automation's stream (typed by Stream[P])

ctx.pool(spec)          # typed pool handle (see concepts/primitives.md)
ctx.submit(spec, msg)   # enqueue a typed task to another BatchAutomation
ctx.kv(namespace)       # per-namespace key/value store with TTL
ctx.lock(name, ...)     # fleet-wide mutex, async context manager
ctx.counter(name)       # atomic fleet-wide counter

ctx.events              # pubsub event bus (publish/subscribe lightweight signals)
ctx.metrics             # per-slot counters, gauges, histograms (slot-local; not shared)
```

`emit`, `stream`, `pool`, and `submit` all take typed reference objects (`Pool[P, T]`, `Queue[P]`, `Stream[P]`) and return typed handles. The reference carries the wire name *and* the Pydantic schema; the framework validates payloads at the boundary. See [contracts](/concepts/contracts.md).

`kv`, `lock`, and `counter` are anonymous — just names — for state that doesn't have a per-record schema. See [primitives](/concepts/primitives.md).

If you find yourself wanting Redis or the master URL inside a slot, that's a sign the framework is missing a primitive. Open an issue.

## Cleanup

Both base classes have an overridable `async def cleanup(self, ctx)` hook. It runs after `run_slot` or `run_one` finishes, whether normally or via exception. Use it to release resources that survive the `run_*` callback's normal `try/finally` boundary — for example, killing leaked subprocesses by name.

The `fleet-browser` pool does exactly this: after closing the browser, it sweeps Linux processes whose command line includes the slot's `--user-data-dir` and kills any that survived. This was the root cause of a multi-day outage in the original Turnstile farm before the framework existed.

## Registration

`@register("name")` does three things:

Sets `cls.automation_type = "name"`.

Adds the class to a module-global registry keyed by name.

Raises if the same name is registered twice with different classes — typo defense.

It does not import anything or talk to the master. The entry-point mechanism handles discovery. Calling `register` outside of a plugin module that gets loaded by `importlib.metadata.entry_points` won't make the master see your automation.


---

# 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/automations.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.
