{% extends "base.html" %} {% block title %}Examples — Waffle{% endblock %} {% block content %}

Examples

Copy-paste recipes for the four ways to run an agent. Hub URLs are filled in for {{ request.url.scheme }}://{{ request.url.netloc }}.

1 · Bootstrap a new agent

Every recipe below assumes a Poetry project with the SDK installed.

mkdir my-agent && cd my-agent
poetry init --no-interaction --name my-agent --python ">=3.11,<4.0"
poetry add heywaffle claude-agent-sdk
agent.py — a handler that asks Claude one question
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, ResultMessage
from waffle_sdk import AgentContext, run


async def handle(item: dict, ctx: AgentContext) -> dict:
    options = ClaudeAgentOptions(
        system_prompt="You are a helpful assistant.",
        permission_mode="bypassPermissions",
    )
    async with ClaudeSDKClient(options=options) as client:
        await client.query(item["prompt"])
        async for msg in client.receive_response():
            if isinstance(msg, ResultMessage):
                return {"reply": msg.result or ""}
    return {"reply": ""}


if __name__ == "__main__":
    run(handle,
        agent="hello",
        project="default",
        subscriptions=[{"source": "hello", "filter": {}}])

2 · Once mode — run the handler locally, no hub needed

For iterating on prompts, debugging tool-use, or calling the agent as a one-off script. The SDK will try to stream the Claude session JSONL to the hub if it's reachable; otherwise it silently runs offline.

poetry run python agent.py once --input '{"prompt":"hi"}'

Force-disable trace streaming:

poetry run python agent.py once --input '{"prompt":"hi"}' --no-trace

Or point at a remote hub:

poetry run python agent.py once --input '{"prompt":"hi"}' \
  --hub {{ request.url.scheme }}://{{ request.url.netloc }}

3 · Inbox mode — persistent worker, triggered by a webhook

The agent connects to the hub, claims items from its queue over SSE, and handles them one (or concurrency) at a time. Items arrive via POST /webhooks/<source> whenever an external system (Linear, GitHub, n8n, your own app) sends a payload that matches the agent's subscriptions filter.

Start the agent (leave running)
poetry run python agent.py inbox \
  --hub {{ request.url.scheme }}://{{ request.url.netloc }}
Trigger it from anywhere
curl -sX POST {{ request.url.scheme }}://{{ request.url.netloc }}/webhooks/hello \
  -H 'content-type: application/json' \
  -d '{"prompt":"what's the capital of Peru?"}'

The hub returns the inbox item id immediately. The result appears in the UI's Inbox view + as a session in Sessions once the handler finishes.

Subscription filters (in your run() call)
subscriptions=[
    # Fire only when the payload contains action:"create"
    {"source": "linear", "filter": {"action": "create"}},
    # Nested keys match by path — type.name == "Issue"
    {"source": "linear", "filter": {"type": {"name": "Issue"}}},
    # Empty filter matches anything from that source
    {"source": "hello",  "filter": {}},
]

4 · Cron mode — run on a schedule

The hub's scheduler fires your agent on a cron expression. Each tick creates an inbox item with source matching the cron's source and a payload you supply. Useful for daily digests, periodic checks, reminders.

if __name__ == "__main__":
    run(handle,
        agent="daily-check",
        project="default",
        crons=[
            {"schedule": "0 9 * * *", "source": "cron", "payload": {"prompt": "morning check"}},
            {"schedule": "0 17 * * 1-5", "source": "cron", "payload": {"prompt": "end-of-day"}},
        ])

Keep the agent running in inbox mode — crons create inbox items, which the agent claims the same way as webhook-triggered items.

5 · Sync API — request / response from a browser or backend

POST /api/invoke/<agent> blocks until the agent finishes and returns the result in one HTTP response — no inbox polling, no SSE. Useful for AI features in a web app, Slack slash commands, anywhere you just need an answer.

curl
curl -sX POST {{ request.url.scheme }}://{{ request.url.netloc }}/api/invoke/hello \
  -H 'content-type: application/json' \
  -d '{"prompt":"summarise Waffle in one sentence"}'
Browser fetch (with token + CORS)
const res = await fetch("{{ request.url.scheme }}://{{ request.url.netloc }}/api/invoke/hello", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "Authorization": "Bearer " + YOUR_TOKEN,    // only needed when WAFFLE_API_TOKEN is set
  },
  body: JSON.stringify({ prompt: "summarise Waffle" }),
});
const data = await res.json();
console.log(data);

The agent must be running in inbox mode when you call /api/invoke — the hub routes the item to a free worker and holds the HTTP response open until it completes.

Enable for browser use (on the hub)
# /etc/systemd/system/waffle.service
Environment=WAFFLE_CORS_ORIGINS=https://app.example.com,http://localhost:3000
Environment=WAFFLE_API_TOKEN=$(openssl rand -hex 32)  # generate once, keep secret

Running more than one agent

Each agent is an independent Python process. On a single machine, run them side by side with a process manager like honcho (a Python port of foreman) — one systemd unit for all agents, add a line to the Procfile for each new one.

# ~/waffle/Procfile
hello:     bash -c 'cd /home/sqrt/agents/hello          && poetry run python agent.py inbox'
triage:    bash -c 'cd /home/sqrt/agents/linear-triage  && poetry run python agent.py inbox'
scraper:   bash -c 'cd /home/sqrt/agents/recipe-scraper && poetry run python agent.py inbox'

See deploy/ in the Waffle repo for a full systemd + Caddy example.

{% endblock %}