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.
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.
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.
# 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
#!/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.
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.
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:
# 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:
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
| Agent | Trigger | Frequency | Per-run cost | Monthly cost |
|---|---|---|---|---|
| Competitive intel | Cron (6am daily) | 30x/month | $0.05 | $1.50 |
| PR reviewer | GitHub webhook | ~100 PRs/month | $0.05 | $5.00 |
| QA on deploy | Deploy webhook | ~100 deploys/month | $0.15 | $15.00 |
| AP vendor portals | Cron (weekly) | 4x/month | $0.50 | $2.00 |
| Slack research | Slack webhook | ~20 questions/month | $0.03 | $0.60 |
| Payment failure | Stripe webhook | ~10 failures/month | $0.02 | $0.20 |
| Monthly reconciliation | Cron (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