> 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/guides/continuous-automation.md).

# Writing a continuous automation

Use this shape when the work is a loop with no natural end. A scraper watching a feed, a CAPTCHA solver producing tokens, a worker that polls an external API every N seconds.

## Skeleton

```python
from pydantic import BaseModel
from fleet.core import BaseConfig, ContinuousAutomation, register

class MyConfig(BaseConfig):
    target: str

class MyResult(BaseModel):
    target: str
    status: int

@register("my-thing")
class MyThing(ContinuousAutomation[MyConfig]):
    Config = MyConfig
    Output = MyResult            # the type ctx.emit() accepts and the stream carries

    async def run_slot(self, ctx):
        while not ctx.shutdown.is_set():
            status = await do_work(ctx.config.target)
            await ctx.emit(MyResult(target=ctx.config.target, status=status))
```

That's the whole API. `run_slot` runs to completion exactly once per slot per lifetime. Loop yourself; the framework won't loop for you.

## The shutdown pattern

Always check `ctx.shutdown` somewhere in the loop. The framework sets it when:

The slot is being recycled (age exceeded `recycle_seconds`).

The worker is shutting down (SIGTERM, SIGINT).

The config disabled the slot.

The reconcile loop is restarting the slot for any other reason.

If you ignore `ctx.shutdown`, the framework waits up to 15 seconds for your coroutine to return before cancelling it. The cancellation fires `asyncio.CancelledError` inside whatever you're awaiting; usually this works fine but it's rude to depend on it.

The cleanest shape:

```python
async def run_slot(self, ctx):
    while not ctx.shutdown.is_set():
        await self.one_iteration(ctx)
        try:
            await asyncio.wait_for(
                ctx.shutdown.wait(),
                timeout=ctx.config.interval_seconds,
            )
        except asyncio.TimeoutError:
            continue
```

The `wait_for` is the sleep. It exits early if shutdown fires, so you don't sit through the full sleep before noticing.

## Emitting

`ctx.emit(payload)` is the only way work product leaves your slot. The payload must be an instance of your automation's declared `Output` Pydantic class; the framework raises `TypeError` if it isn't. Once validated, it serializes to JSON and pushes onto your automation's output stream.

Consumers of your stream — other automations, the REST `pop` endpoint, the dashboard — receive entries typed against the same `Output` schema.

The stream is bounded; see [streams](/concepts/streams-and-events.md). Don't assume entries will sit there waiting for you forever.

## Metrics

`ctx.metrics` gives you counters, gauges, and histograms:

```python
ctx.metrics.incr("requests_total")
ctx.metrics.incr("errors_total", by=1)
ctx.metrics.set("queue_depth", queue.qsize())
ctx.metrics.observe("request_latency_ms", elapsed)
```

The stats reporter aggregates these across all slots in the worker every 15 seconds and includes them in the Stats frame sent to the master. The dashboard surfaces them.

Counters keep going up. Gauges are point-in-time. Histograms keep the last 512 samples and report p50/p95/p99.

## Errors

The framework catches exceptions from `run_slot` and logs them. Your slot ends. The next recycle tick spawns a replacement.

If your slot is crashy, this works. If your slot crashes consistently because of a bug in your config (e.g., a malformed target URL), this will spawn and crash forever. Watch the worker logs and the `last_error` field on the worker record.

The recycle tick is currently every 1-30 seconds depending on `recycle_seconds`. A consistently crashy slot won't peg CPU but it will spam logs. If you want a backoff, implement it inside `run_slot` and rely on `ctx.shutdown` to break out.

## Resources

Anything you open in `run_slot` — connections, files, browser handles — should be closed in a `try/finally`. The slot can be cancelled mid-loop, and your `finally` is the only thing the framework will let run before tearing the task down.

For resources that need cleanup beyond the `try/finally` boundary (e.g., killing leaked subprocesses), override `cleanup(self, ctx)`. It runs after `run_slot` returns or raises, regardless of how.

```python
class MyThing(ContinuousAutomation[MyConfig]):
    async def run_slot(self, ctx):
        async with open_resource() as r:
            while not ctx.shutdown.is_set():
                ...

    async def cleanup(self, ctx):
        # only needed if open_resource() leaks subprocesses
        kill_orphans_by_marker(ctx.slot_id)
```

## Real example

The `hello-world` example in `examples/hello-world/` is a fully working continuous automation in about 25 lines. Read it. It does nothing useful, but it covers every pattern in this guide.

For a real one, see the [browser-based automations](/guides/browser-automations.md) guide, which walks through porting the original Turnstile-solver to this framework.


---

# 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/guides/continuous-automation.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.
