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

Examples

Copy-paste recipes for the five 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
pyproject.toml — point Waffle at your Agent instance
[tool.waffle]
agent = "my_agent.agent:app"
src/my_agent/agent.py — Agent + a webhook handler
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, ResultMessage
from waffle_sdk import Agent, AgentContext

app = Agent("hello", project="default")


@app.webhook(source="hello")
async def reply(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": ""}

No __main__.py, no [project.scripts] entry — the unified waffle CLI discovers the agent via the [tool.waffle] block above.

2 · waffle test — run a 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.

List the discovered handlers
cd ~/path/to/my-agent
poetry run waffle test
Run one with a payload
poetry run waffle test reply --input '{"prompt":"hi"}'

Force-disable trace streaming:

poetry run waffle test reply --input '{"prompt":"hi"}' --no-trace

Or point at a remote hub:

poetry run waffle test reply --input '{"prompt":"hi"}' \
  --hub {{ request.url.scheme }}://{{ request.url.netloc }}

3 · waffle serve — persistent worker, triggered by webhooks

The agent connects to the hub, claims items from its queue over SSE, and dispatches them to the right decorated handler. Items arrive via POST /webhooks/<source> whenever an external system (Linear, GitHub, n8n, your own app) sends a payload that matches the source + filter declared on a @app.webhook.

Start the agent (leave running)
cd ~/path/to/my-agent
poetry run waffle serve --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 is 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 on the decorator
# Fire only when the payload contains action:"create"
@app.webhook(source="linear", filter={"action": "create"})
async def on_linear_create(item, ctx): ...

# Nested keys match by dot-path — type.name == "Issue"
@app.webhook(source="linear", filter={"type.name": "Issue"})
async def on_issue(item, ctx): ...

# Empty filter (or omitting it) matches everything from that source
@app.webhook(source="hello")
async def on_hello(item, ctx): ...

4 · @app.cron — run on a schedule

The hub's scheduler fires your handler on a cron expression. Each tick creates an inbox item with source = "cron:<handler_name>" (auto-generated, so multiple cron handlers in one agent route correctly) and the payload you declared.

@app.cron("0 9 * * *", payload={"prompt": "morning check"})
async def morning_check(item, ctx):
    ...

@app.cron("0 17 * * 1-5", payload={"prompt": "end-of-day"})
async def end_of_day(item, ctx):
    ...

Keep the agent running with waffle serve — crons create inbox items, which the SDK dispatches the same way as webhook-triggered items.

Test a cron handler locally with its declared payload as the default:

poetry run waffle test morning_check        # uses the @app.cron payload
poetry run waffle test morning_check --input '{"prompt":"override"}'

5 · @app.task — sync request / response API

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. Declare the handler with @app.task().

# Optional: typed input. Bad payloads dead-letter immediately, no retries.
from pydantic import BaseModel

class AskRequest(BaseModel):
    prompt: str

@app.task()
async def ask(item: AskRequest, ctx) -> dict:
    options = ClaudeAgentOptions(system_prompt="Be terse.")
    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": ""}
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 via waffle serve when you call /api/invoke — the hub routes the item to a free worker and holds the HTTP response open until it completes. Maximum one @app.task per agent in v0.3.

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

6 · waffle deploy — ship the agent to the hub as a Docker container

Once the handler works, push it to the hub so it runs as a managed Docker container next to the hub itself. No separate VPS, no Procfile, no babysitting waffle serve in a tmux session. The hub stops + replaces the previous container, restarts on crash, and exposes a Deployments view with live logs.

Add a deploy block to pyproject.toml
[tool.waffle.deploy]
env_keys = ["ANTHROPIC_API_KEY", "LINEAR_TOKEN"]   # secrets to inject at start
memory   = "512m"
cpus     = "1"
Stash the secrets on the hub (encrypted-at-rest)
poetry run waffle secrets set hello \
    ANTHROPIC_API_KEY=sk-ant-... \
    LINEAR_TOKEN=lin_xxx \
    --hub {{ request.url.scheme }}://{{ request.url.netloc }}

poetry run waffle secrets list hello \
    --hub {{ request.url.scheme }}://{{ request.url.netloc }}

Values are encrypted on the hub host and never returned over HTTP — only injected as env vars when the container starts.

Deploy
poetry run waffle deploy --hub {{ request.url.scheme }}://{{ request.url.netloc }}

First build is ~90s (apt installs nodejs+npm so MCP servers via npx work out of the box); redeploys are fast thanks to layer cache. Container runs as non-root agent UID 1000, with --restart=unless-stopped and labels waffle.managed=true so it doesn't collide with other apps on the host.

Hub-side prereqs (Docker daemon, --host 0.0.0.0, ufw rule, Caddy SSE config) live in WAFFLE_DOCKER.md.

Running more than one agent

Two routes — pick per agent:

  • Native via systemd + honcho — each agent is its own Python process, one systemd unit manages them all via a Procfile. Add a line per agent.
  • waffle deploy — the hub manages the container's lifecycle. Restart/Stop/Delete from the Deployments page; logs in the UI.
Procfile (honcho)
# ~/waffle/Procfile
hello:     bash -c 'cd /home/sqrt/agents/hello          && poetry run waffle serve'
triage:    bash -c 'cd /home/sqrt/agents/linear-triage  && poetry run waffle serve'
scraper:   bash -c 'cd /home/sqrt/agents/recipe-scraper && poetry run waffle serve'

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

{% endblock %}