> 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/getting-started/first-automation.md).

# Your first automation

You're going to build an automation called `url-pinger` that takes a URL from its config and pings it every few seconds, emitting the response status as output. It's a deliberately boring example — the point is the framework, not the work.

## 1. Package layout

```
url-pinger/
  pyproject.toml
  url_pinger/
    __init__.py
```

## 2. pyproject.toml

```toml
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "url-pinger"
version = "0.0.1"
requires-python = ">=3.11"
dependencies = ["fleet-core", "httpx"]

[project.entry-points."fleet.automations"]
url-pinger = "url_pinger:UrlPinger"

[tool.setuptools.packages.find]
where = ["."]
```

The entry-point line is the only thing that makes Fleet aware of your plugin. The key on the left (`url-pinger`) is the automation type name you'll use in URLs and CLI flags. The value on the right (`url_pinger:UrlPinger`) is the import path to the automation class.

## 3. The automation

`url_pinger/__init__.py`:

```python
import asyncio
import httpx

from fleet.core import BaseConfig, ContinuousAutomation, register


class UrlPingerConfig(BaseConfig):
    url: str = "https://example.com"
    interval_seconds: float = 5.0
    timeout_seconds: float = 10.0


@register("url-pinger")
class UrlPinger(ContinuousAutomation[UrlPingerConfig]):
    Config = UrlPingerConfig

    async def run_slot(self, ctx):
        client = httpx.AsyncClient(
            timeout=ctx.config.timeout_seconds,
            proxy=ctx.proxy,
        )
        try:
            while not ctx.shutdown.is_set():
                try:
                    resp = await client.get(ctx.config.url)
                    await ctx.emit({
                        "url": ctx.config.url,
                        "status": resp.status_code,
                        "elapsed_ms": int(resp.elapsed.total_seconds() * 1000),
                    })
                    ctx.metrics.incr("pings_ok")
                except Exception as e:
                    ctx.metrics.incr("pings_failed")
                    ctx.logger.warning("ping failed: %s", e)
                try:
                    await asyncio.wait_for(
                        ctx.shutdown.wait(),
                        timeout=ctx.config.interval_seconds,
                    )
                except asyncio.TimeoutError:
                    pass
        finally:
            await client.aclose()
```

Three things to notice:

`BaseConfig` already gives you `enabled`, `slots`, `recycle_seconds`, and `proxy_url`. You add only the fields your plugin actually needs.

`ctx.shutdown` is an `asyncio.Event`. The framework sets it when the slot is being recycled or the worker is stopping. The loop checks it on every iteration and the `wait_for(shutdown.wait(), timeout=...)` pattern lets the interval also be a graceful-exit point.

`ctx.proxy` is a convenience for the `proxy_url` field from `BaseConfig`. The framework doesn't apply it for you; the plugin decides what to do with it. Here we pass it to `httpx`.

## 4. Install and verify

```bash
pip install -e .
fleet info
# url-pinger    url_pinger.UrlPinger    (ContinuousAutomation)
```

If `url-pinger` doesn't appear, either the install didn't pick up the entry-point or the `@register` decorator never ran. The decorator only runs when the module is imported; entry-point discovery handles that for you, but only if the entry-point points at the right import path.

## 5. Run it

Assuming the master is running from the [quickstart](/getting-started/quickstart.md):

```bash
# in the worker terminal:
fleet worker --type url-pinger --id pinger-1

# in another terminal:
curl -X PATCH http://localhost:8000/api/v1/automations/url-pinger/workers/pinger-1/config \
  -H "Authorization: Bearer $FLEET_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "slots": 1, "url": "https://httpbin.org/status/200", "interval_seconds": 2}'

# pop a few results:
curl -X POST http://localhost:8000/api/v1/automations/url-pinger/output/pop \
  -H "Authorization: Bearer $FLEET_ADMIN_TOKEN" \
  -d '{"n": 3}' -H "Content-Type: application/json"
```

Output:

```json
[
  {"id": "...", "slot_id": 0, "ts": ..., "payload": {"url": "https://httpbin.org/status/200", "status": 200, "elapsed_ms": 142}},
  ...
]
```

## What you skipped

A bunch of stuff the framework handled for you:

WebSocket connection from the worker to the master with auto-reconnect and outbound buffering.

Config validation against your Pydantic schema before applying.

The reconcile loop. Bumping `slots` from 1 to 3 would have spawned two more `run_slot` tasks; the framework's reconcile diff figured out that "scale up by 2" is all that's needed, no restart.

The output stream. `ctx.emit` does a `ZADD` with a UUID-keyed member and publishes a pubsub event. The stream is bounded by length and TTL so it can't grow forever.

Stats reporting and per-slot metrics. The dashboard reads these on a 5-second poll.

Auth on every endpoint (admin token for management, worker token for poll and WS).

Recycle and heal. If your slot crashes, the next reconcile tick respawns it. If the worker hits its `recycle_seconds`, the oldest slot gets torn down and replaced.

You wrote about 30 lines.


---

# 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/getting-started/first-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.
