Skip to main content

YOUR FIRST AUTOMATION

In this tutorial, we will build a health monitor that checks three HTTP endpoints, runs the checks in parallel, summarizes the results, and keeps a queryable history of every run.

What we will build

We are going to create a typed extension called @tutorial/http-check that measures response status, latency, and availability for any URL. We will define three monitors from that single type — one for a real site, one for example.com, and one that always fails. A workflow will run all three checks in parallel and print a summary report. Every result is versioned, so we can query it later.

Install Swamp

Open your terminal and run:

$ curl -fsSL https://swamp-club.com/install.sh | sh

Once it finishes, verify the installation:

$ swamp version

You will see output like:

info    version              swamp "20260727.150514.0-sha...."

If the command is not found, see the install guide for troubleshooting.

Initialize the repo

Create a new directory and initialize it as a Swamp repo:

$ mkdir health-monitor
$ cd health-monitor
$ swamp repo init

You will see the Swamp banner:

    ███████╗██╗    ██╗ █████╗ ███╗   ███╗██████╗
    ██╔════╝██║    ██║██╔══██╗████╗ ████║██╔══██╗
    ███████╗██║ █╗ ██║███████║██╔████╔██║██████╔╝
    ╚════██║██║███╗██║██╔══██║██║╚██╔╝██║██╔═══╝
    ███████║╚███╔███╔╝██║  ██║██║ ╚═╝ ██║██║
    ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚═╝

    ╔═══════════════════════════════════════════╗
    ║  WELCOME TO THE CLUB                      ║
    ║  for hackers, by hackers                  ║
    ╚═══════════════════════════════════════════╝

info    repo·init            Initialized swamp repository at "..." (tool: "claude")

Notice the .claude/ directory and CLAUDE.md file — these are what let Claude Code work with Swamp automatically.

$ ls -a
.  ..  .claude  CLAUDE.md  .gitignore  models  .swamp  .swamp.yaml  vaults  workflows

Create the check type

We need a typed extension that sends an HTTP request and records the result.

Note

In practice, you would ask your agent to create this extension. We include the full source here so you can see the typed result at each step.

Create the file extensions/models/http_check.ts:

$ mkdir -p extensions/models
// extensions/models/http_check.ts
import { z } from "npm:zod@4";

const GlobalArgsSchema = z.object({
  url: z.string().url().describe("The URL to check"),
  timeout: z.number().positive().default(5000).describe(
    "Request timeout in milliseconds",
  ),
});

const ResultSchema = z.object({
  status: z.number().describe("HTTP status code, or 0 if the request failed"),
  latency: z.number().describe("Response time in milliseconds"),
  ok: z.boolean().describe("True when status is 2xx and the request completed"),
});

export const model = {
  type: "@tutorial/http-check",
  version: "2026.07.28.1",

  globalArguments: GlobalArgsSchema,

  resources: {
    result: {
      description: "Health check result for a single endpoint",
      schema: ResultSchema,
      lifetime: "infinite" as const,
      garbageCollection: 100,
    },
  },

  methods: {
    check: {
      description:
        "Send an HTTP GET to the configured URL and record the result",
      arguments: z.object({}),
      // deno-lint-ignore no-explicit-any
      async execute(_args: Record<string, never>, context: any) {
        const { url, timeout } = context.globalArgs;
        const start = Date.now();
        let status = 0;
        let ok = false;

        try {
          const response = await fetch(url, {
            signal: AbortSignal.timeout(timeout),
          });
          status = response.status;
          ok = response.ok;
        } catch {
          // Connection refused, timeout, DNS failure — status stays 0
        }

        const latency = Date.now() - start;

        const result = { status, latency, ok };
        const handle = await context.writeResource("result", "result", result);

        return { dataHandles: [handle] };
      },
    },
  },
};

Verify that Swamp discovers the type:

$ swamp model type describe @tutorial/http-check

You will see:

Type: @tutorial/http-check
Version: 2026.07.28.1

Global Arguments:
  url (string) *required
  timeout (number)

Data Outputs:
  result [resource] - Health check result for a single endpoint (infinite)

Methods:
  check - Send an HTTP GET to the configured URL and record the result

Three definitions from one type

Now create three model definitions — one per endpoint we want to monitor:

$ swamp model create @tutorial/http-check check-swamp-club \
    --global-arg 'url=https://swamp-club.com' \
    --global-arg 'timeout=5000'

You will see:

Created: check-swamp-club (@tutorial/http-check)
$ swamp model create @tutorial/http-check check-example \
    --global-arg 'url=https://example.com' \
    --global-arg 'timeout=5000'
Created: check-example (@tutorial/http-check)

For the third, use http://localhost:9 — a port that nothing listens on, so the check will always fail:

$ swamp model create @tutorial/http-check check-dead \
    --global-arg 'url=http://localhost:9' \
    --global-arg 'timeout=2000'
Created: check-dead (@tutorial/http-check)

Notice that all three definitions share the same @tutorial/http-check type but have different URLs and timeouts.

Run one check manually

Run the example.com check to see the typed result:

$ swamp model method run check-example check

You will see output like:

   Resolved   check-example (@tutorial/http-check)
  Executing   check

  Completed   check on check-example succeeded in 187ms

  ┌───────────────────────────────────┐
  │ Arguments                         │
  │    url       https://example.com  │
  │    timeout   5000                 │
  │                                   │
  │ Data produced                     │
  │  result                           │
  │    status    200                  │
  │    latency   176                  │
  │    ok        true                 │
  │                                   │
  └───────────────────────────────────┘

Now inspect the stored data:

$ swamp data get check-example result --version 1
Data: result (v1)
Model: check-example (@tutorial/http-check)
Content: application/json, 38B
Lifetime: infinite | GC: 100
Tags: type=resource, specName=result, modelName=check-example

{
  "status": 200,
  "latency": 176,
  "ok": true
}

Notice the status, latency, and ok fields matching the schema we defined.

Wire the workflow

We will create a workflow with two jobs: a check job that runs all three monitors in parallel, and a summarize job that reads their results using CEL expressions. First, create a shell model for the summary step:

$ swamp model create command/shell health-summary
Created: health-summary (command/shell)

Now create the workflow:

$ swamp workflow create health-check
Created: health-check

Open the workflow file that was created under workflows/ and replace its contents with:

id: <keep the generated id>
name: health-check
tags: {}
jobs:
  - name: check
    description: Check all endpoints in parallel
    steps:
      - name: swamp-club
        description: Check swamp-club.com
        task:
          type: model_method
          modelIdOrName: check-swamp-club
          methodName: check
        dependsOn: []
        weight: 0
        allowFailure: false
      - name: example
        description: Check example.com
        task:
          type: model_method
          modelIdOrName: check-example
          methodName: check
        dependsOn: []
        weight: 1
        allowFailure: false
      - name: dead
        description: Check dead endpoint
        task:
          type: model_method
          modelIdOrName: check-dead
          methodName: check
        dependsOn: []
        weight: 2
        allowFailure: false
    dependsOn: []
    weight: 0
  - name: summarize
    description: Summarize results
    steps:
      - name: report
        description: Print a summary of all checks
        task:
          type: model_method
          modelIdOrName: health-summary
          methodName: execute
          inputs:
            run: |
              echo "=== Health Check Report ==="
              echo ""
              echo "swamp-club.com: status=${{ data.latest("check-swamp-club", "result").attributes.status }}, latency=${{ data.latest("check-swamp-club", "result").attributes.latency }}ms, ok=${{ data.latest("check-swamp-club", "result").attributes.ok }}"
              echo "example.com:    status=${{ data.latest("check-example", "result").attributes.status }}, latency=${{ data.latest("check-example", "result").attributes.latency }}ms, ok=${{ data.latest("check-example", "result").attributes.ok }}"
              echo "localhost:9:    status=${{ data.latest("check-dead", "result").attributes.status }}, latency=${{ data.latest("check-dead", "result").attributes.latency }}ms, ok=${{ data.latest("check-dead", "result").attributes.ok }}"
        dependsOn: []
        weight: 0
        allowFailure: false
    dependsOn:
      - job: check
        condition:
          type: completed
    weight: 1
version: 1

Run the workflow

$ swamp workflow run health-check

You will see output like:

    system │ Starting workflow health-check

     check │ start
     check │ step example · check-example · check · start
     check │ step swamp-club · check-swamp-club · check · start
     check │ step dead · check-dead · check · start
     check │ done dead in 31ms
     check │ done example in 186ms
     check │ done swamp-club in 3.1s
     check │ completed in 3.1s
 summarize │ start
 summarize │ step report · health-summary · execute · start
 summarize │ === Health Check Report ===
 summarize │
 summarize │ swamp-club.com: status=200, latency=3020ms, ok=true
 summarize │ example.com:    status=200, latency=160ms, ok=true
 summarize │ localhost:9:    status=0, latency=0ms, ok=false
 summarize │ done report in 30ms
 summarize │ completed in 30ms

    system │ Completed workflow health-check succeeded in 3.1s

Notice that the three checks in the check job all started at the same time, and the summarize job only started after they finished. The dead endpoint shows status=0, ok=false.

Query the history

Run the workflow again to build up history:

$ swamp workflow run health-check

Now check how many versions have accumulated for the dead endpoint:

$ swamp data versions check-dead result

You will see multiple versions, each with a timestamp:

{
  "dataName": "result",
  "modelName": "check-dead",
  "modelType": "@tutorial/http-check",
  "versions": [
    { "version": 3, "createdAt": "2026-07-27T23:54:32.221Z", "isLatest": true },
    {
      "version": 2,
      "createdAt": "2026-07-27T23:54:19.350Z",
      "isLatest": false
    },
    { "version": 1, "createdAt": "2026-07-27T23:52:58.649Z", "isLatest": false }
  ],
  "total": 3
}

Notice that each run added a new version. Now query across all models to find which checks failed:

$ swamp data query 'attributes.ok == false'
┌────────┬────────────┬──────────┬──────────┬─────────┬──────┐
│ name   │ modelName  │ specName │ dataType │ version │ size │
├────────┼────────────┼──────────┼──────────┼─────────┼──────┤
│ result │ check-dead │ result   │ resource │ 3       │ 35B  │
└────────┴────────────┴──────────┴──────────┴─────────┴──────┘

1 result

Only check-dead appears — the query matched across every model in the repo.

What we built

We built an endpoint health monitor: a typed extension, three model definitions, a workflow with parallel checks and a summary, and versioned history we can query with CEL.

Next steps

  • Storing Secrets — add an API token for an authenticated endpoint using an encrypted vault.
  • Publish an Extension — package @tutorial/http-check as a community extension others can install.