Guide #12 Use Case — Architecture 22 min read March 2026

Event-Driven Agents: Work That Starts Itself

The most powerful AI agents don't wait for someone to type a prompt. They react to the world: a Slack message, a GitHub PR, a Stripe payment failure, a calendar reminder, a cron schedule. Each event spins up a sandbox, the agent does its work, posts results, and shuts down. The human never types a prompt. The agent just does its job.

From prompt-driven to event-driven

Every AI agent guide starts the same way: "Type a prompt, the agent does the work." That's the demo. It's not how real employees work.

A real employee doesn't sit at their desk waiting for someone to walk over and describe a task. They react to events. An email arrives — they respond. A calendar reminder fires — they prepare for the meeting. A deploy completes — they check the dashboard. A customer files a ticket — they investigate. The work comes to them.

AI agents should work the same way. The infrastructure to make this happen is straightforward: a webhook receiver that listens for events, a dispatcher that matches events to agents, and the sandbox platform that provides the compute. The agent spins up, does the work, reports results, and shuts down. No human in the loop.

Slack message GitHub PR Cron (6am daily) SQS queue Calendar event
↓ event ↓
Event dispatcher
↓ creates sandbox + assigns task ↓
Agent does the work
↓ results ↓
Slack post PR comment Email report S3 upload
Sandbox created on event, destroyed after task completes. Pay only for active work.

The four trigger patterns

Pattern 1: Webhooks

An external system sends an HTTP POST to your webhook endpoint. Your handler parses the payload and kicks off the appropriate agent.

python — webhook_dispatcher.py
from fastapi import FastAPI, Request
import asyncio, httpx, json, os

app = FastAPI()
SANDBOX_URL = os.environ["SANDBOX_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


async def run_agent_task(task_name: str, command: str):
    """Create a sandbox, run a task, clean up."""
    async with httpx.AsyncClient(timeout=600) as client:
        # Create
        sandbox = (await client.post(f"{SANDBOX_URL}/api/sandboxes",
            headers=HEADERS,
            json={"name": task_name, "instance_type": "ab0t.micro", "auto_stop_minutes": 15}
        )).json()
        sid = sandbox["sandbox_id"]

        # Wait for boot
        for _ in range(24):
            s = (await client.get(f"{SANDBOX_URL}/api/sandboxes/{sid}", headers=HEADERS)).json()
            if s["status"] == "running": break
            await asyncio.sleep(5)

        # Execute
        result = (await client.post(f"{SANDBOX_URL}/api/sandboxes/{sid}/execute",
            headers=HEADERS, json={"command": command})).json()

        # Cleanup
        await client.delete(f"{SANDBOX_URL}/api/sandboxes/{sid}", headers=HEADERS)

        return result


# --- Slack: someone asks a question in #ask-agent ---
@app.post("/webhook/slack")
async def handle_slack(request: Request):
    payload = await request.json()
    if payload.get("type") == "event_callback":
        event = payload["event"]
        if event.get("type") == "message" and not event.get("bot_id"):
            question = event["text"]
            channel = event["channel"]
            asyncio.create_task(slack_research_agent(question, channel))
    return {"challenge": payload.get("challenge", "")}


async def slack_research_agent(question, channel):
    result = await run_agent_task(
        "slack-research",
        f"pip install httpx anthropic && python research.py '{question}'")
    # Post result back to Slack
    await post_to_slack(channel, result["stdout"])


# --- GitHub: PR opened ---
@app.post("/webhook/github")
async def handle_github(request: Request):
    payload = await request.json()
    event = request.headers.get("X-GitHub-Event")

    if event == "pull_request" and payload["action"] in ("opened", "synchronize"):
        pr = payload["pull_request"]
        repo = payload["repository"]["full_name"]
        asyncio.create_task(pr_review_agent(repo, pr["number"], pr["head"]["ref"]))

    return {"status": "ok"}


async def pr_review_agent(repo, pr_number, branch):
    result = await run_agent_task(
        f"pr-review-{pr_number}",
        f"git clone https://github.com/{repo} project && cd project && git checkout {branch} && npm test")
    # Post comment on PR
    await post_github_comment(repo, pr_number, result["stdout"])


# --- Stripe: payment failed ---
@app.post("/webhook/stripe")
async def handle_stripe(request: Request):
    payload = await request.json()
    if payload["type"] == "invoice.payment_failed":
        customer_id = payload["data"]["object"]["customer"]
        asyncio.create_task(payment_failure_agent(customer_id))
    return {"status": "ok"}


async def payment_failure_agent(customer_id):
    # Agent logs into billing portal, checks customer status,
    # posts summary to #billing-alerts Slack channel
    result = await run_agent_task(
        "payment-failure",
        f"python investigate_payment.py {customer_id}")
    await post_to_slack("billing-alerts", result["stdout"])

Pattern 2: Cron / scheduled triggers

Time-based triggers for recurring work. Use systemd timers, AWS EventBridge rules, or any cron scheduler.

bash — crontab examples
# Daily at 6am: competitive intelligence sweep
0 6 * * * /opt/agents/run-agent.sh competitive-intel

# Every Monday at 7am: AP vendor portal run
0 7 * * 1 /opt/agents/run-agent.sh weekly-ap

# Every 15 minutes: data quality check
*/15 * * * * /opt/agents/run-agent.sh data-quality-check

# 28th of every month: accounts reconciliation
0 8 28 * * /opt/agents/run-agent.sh monthly-reconciliation

# Every Friday at 5pm: weekly summary report
0 17 * * 5 /opt/agents/run-agent.sh weekly-summary
bash — run-agent.sh (generic wrapper)
#!/bin/bash
# Generic agent runner: create sandbox, run task, cleanup
TASK_NAME=$1

SID=$(curl -s -X POST "$SANDBOX_URL/api/sandboxes" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"$TASK_NAME\",\"instance_type\":\"ab0t.micro\",\"auto_stop_minutes\":15}" \
  | jq -r '.sandbox_id')

# Wait for boot
while [ "$(curl -s "$SANDBOX_URL/api/sandboxes/$SID" \
  -H "Authorization: Bearer $API_KEY" | jq -r '.status')" != "running" ]; do
  sleep 5
done

# Run the task (each task has its own repo/script)
curl -s -X POST "$SANDBOX_URL/api/sandboxes/$SID/execute" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"command\":\"cd /workspace && git clone https://github.com/your-org/agent-tasks.git . && bash tasks/$TASK_NAME.sh\"}"

# Cleanup
curl -s -X DELETE "$SANDBOX_URL/api/sandboxes/$SID" \
  -H "Authorization: Bearer $API_KEY"

Pattern 3: SQS / EventBridge / queues

For AWS-native event streams. A Lambda function consumes from an SQS queue and kicks off sandbox tasks.

python — lambda_handler.py
import json, os, urllib3

SANDBOX_URL = os.environ["SANDBOX_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]
http = urllib3.PoolManager()


def handler(event, context):
    for record in event["Records"]:
        body = json.loads(record["body"])

        # Route by event type
        if body["type"] == "new_s3_object":
            task = f"aws s3 cp {body['s3_uri']} /workspace/input.csv && python process.py"
        elif body["type"] == "customer_signup":
            task = f"python onboard_customer.py {body['customer_id']}"
        elif body["type"] == "alarm_triggered":
            task = f"python investigate_alarm.py '{body['alarm_name']}'"
        else:
            continue

        # Create sandbox and run
        resp = http.request("POST", f"{SANDBOX_URL}/api/sandboxes",
            headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
            body=json.dumps({"name": body["type"], "instance_type": "ab0t.micro", "auto_stop_minutes": 10}))
        sandbox = json.loads(resp.data)

        # Execute (fire and forget — auto_stop handles cleanup)
        # In production, poll for completion before processing next message

Pattern 4: Calendar-aware agents

The agent has its own calendar. It knows that quarter-end compliance is due March 31st. It knows the board meeting is Thursday so the metrics dashboard needs updating by Wednesday. It plans its own work.

python — calendar_agent.py
from datetime import datetime, timedelta
import json

# Agent's task calendar
CALENDAR = [
    {"trigger": "day_of_month", "day": 28, "task": "monthly-reconciliation",
     "description": "Run month-end accounts reconciliation"},
    {"trigger": "day_of_week", "day": "wednesday", "task": "board-metrics",
     "description": "Update board metrics dashboard (board meets Thursday)"},
    {"trigger": "date", "date": "2026-03-31", "task": "q1-compliance",
     "description": "File Q1 compliance report (deadline: March 31)"},
    {"trigger": "days_before", "reference": "2026-03-31", "days": 3,
     "task": "compliance-prep",
     "description": "Prepare Q1 compliance data (deadline in 3 days)"},
]


def get_todays_tasks() -> list:
    """Check what tasks are due today."""
    today = datetime.utcnow()
    tasks = []

    for entry in CALENDAR:
        trigger = entry["trigger"]
        if trigger == "day_of_month" and today.day == entry["day"]:
            tasks.append(entry)
        elif trigger == "day_of_week" and today.strftime("%A").lower() == entry["day"]:
            tasks.append(entry)
        elif trigger == "date" and today.strftime("%Y-%m-%d") == entry["date"]:
            tasks.append(entry)
        elif trigger == "days_before":
            ref = datetime.strptime(entry["reference"], "%Y-%m-%d")
            if (ref - today).days == entry["days"]:
                tasks.append(entry)

    return tasks

# Run daily at 6am: check calendar, execute due tasks
tasks = get_todays_tasks()
for task in tasks:
    print(f"Running: {task['task']} — {task['description']}")
    # run_agent_task(task["task"], ...)

The event dispatcher pattern

In production, you want a single dispatcher that routes all events to the right agent. Here's the architecture:

python — dispatcher.py (complete)
# Event routing table: event type → agent task
ROUTES = {
    "slack.message.ask-agent":    {"task": "slack-research",     "instance": "ab0t.micro"},
    "github.pull_request":        {"task": "pr-review",          "instance": "ab0t.small"},
    "stripe.payment_failed":      {"task": "payment-investigate", "instance": "ab0t.micro"},
    "s3.object_created":          {"task": "process-upload",     "instance": "ab0t.medium"},
    "cron.daily.competitive":     {"task": "competitive-intel",  "instance": "ab0t.micro"},
    "cron.weekly.ap":             {"task": "weekly-ap",         "instance": "ab0t.micro"},
    "cron.monthly.reconciliation": {"task": "reconciliation",    "instance": "ab0t.medium"},
    "calendar.board-prep":        {"task": "board-metrics",     "instance": "ab0t.micro"},
}

async def dispatch(event_type: str, payload: dict):
    route = ROUTES.get(event_type)
    if not route:
        print(f"Unknown event: {event_type}")
        return

    await run_agent_task(
        task_name=route["task"],
        instance_type=route["instance"],
        command=f"cd /workspace && git clone $AGENT_REPO . && bash tasks/{route['task']}.sh",
        env=payload,  # Pass event payload as env vars
    )

Handling failures: dead letters and retries

When an event-driven agent fails, you need to know. Set up a dead-letter pattern:

python
async def run_with_retry(event_type, payload, max_retries=2):
    for attempt in range(max_retries + 1):
        try:
            result = await dispatch(event_type, payload)
            if result and result.get("exit_code") == 0:
                return result
        except Exception as e:
            print(f"Attempt {attempt+1} failed: {e}")

        if attempt < max_retries:
            await asyncio.sleep(2 ** attempt * 5)

    # All retries exhausted — dead letter
    await post_to_slack("agent-failures",
        f"*Agent failed after {max_retries+1} attempts*\nEvent: {event_type}\nPayload: {json.dumps(payload)[:500]}")
    # Optionally write to SQS dead letter queue for manual review

What event-driven agents cost

AgentTriggerFrequencyPer-run costMonthly cost
Competitive intelCron (6am daily)30x/month$0.05$1.50
PR reviewerGitHub webhook~100 PRs/month$0.05$5.00
QA on deployDeploy webhook~100 deploys/month$0.15$15.00
AP vendor portalsCron (weekly)4x/month$0.50$2.00
Slack researchSlack webhook~20 questions/month$0.03$0.60
Payment failureStripe webhook~10 failures/month$0.02$0.20
Monthly reconciliationCron (28th)1x/month$0.20$0.20
Total AI workforce (7 agents) $24.50/month

Seven AI agents running autonomously, reacting to events, for $24.50/month. No prompting. No babysitting. Each sandbox spins up when needed and destroys itself when done.

Production tips

Idempotency

Events can be delivered more than once (webhooks retry, SQS has at-least-once delivery). Make sure your agent tasks are idempotent — running the same task twice with the same input should produce the same result, not duplicate data.

Debounce webhook bursts

A GitHub force-push triggers multiple synchronize events. A Slack thread generates a message event per reply. Debounce by keying on the event source (PR number, thread ID) and canceling the previous run if a new event arrives within 30 seconds.

Use auto_stop_minutes as a safety net

Every sandbox should have auto_stop_minutes set. If the agent crashes, the cleanup code doesn't run, and the sandbox would run forever without it. 10–15 minutes is a good default for event-driven tasks.

Log everything to a central channel

Every agent run should post a one-line summary to a #agent-activity Slack channel: agent name, trigger, duration, outcome (success/failure). This gives you a real-time feed of your AI workforce's activity.

Monitor dead letters

Set up a PagerDuty or Slack alert on the dead letter queue/channel. If an agent fails 3 times, someone should look at it within 24 hours.


What's next

Your agents work while you sleep

Webhooks, cron, queues, calendars. Events trigger the work. Sandboxes provide the compute. Results arrive automatically.

Get Started Free