Alejandro Rioja.
AI Agents

Event-Triggered vs Scheduled Agents: Which Pattern for Which Job

Alejandro Rioja
Alejandro Rioja
7 min read
TL;DR

Use event-triggered agents when a user action demands an immediate response—anything over a few seconds of lag and the experience breaks. Use scheduled agents for batch or periodic work where timing is predictable. The constraint: event-triggered agents must be stateless and fast; scheduled agents can afford to be stateful and slow.

Free newsletter

Every Wednesday. 28,400+ operators. Zero fluff.

Table of contents

Open Table of contents

The two patterns in plain English

An event-triggered agent wakes up because something happened. A booking came in. A comment was posted. A form was submitted. The trigger is external and unpredictable in timing. Your job is to respond fast.

A scheduled agent wakes up because the clock said so. Every morning at 7am. Every Sunday at 6pm. Every hour on the hour. The trigger is internal and completely predictable. Your job is to do thorough work.

That’s it. Don’t overthink it. The architecture follows from the answer to one question: does the user or system need a response right now, or can this wait until a specific time?

Event-triggered: the social reply agent

My social reply agent fires whenever a new comment hits a monitored Facebook post. The agent reads the comment, classifies intent (question, complaint, compliment, spam), drafts a reply, and posts it—or flags it for human review if the confidence is low.

The whole round trip needs to finish in under 30 seconds or the reply feels stale. That’s an event-triggered problem.

Here’s a stripped-down Cloudflare Worker that handles the webhook from a social monitoring service:

typescript
// workers/social-reply.ts
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    // Verify the webhook signature
    const sig = request.headers.get("x-webhook-signature") ?? "";
    const body = await request.text();
    const valid = await verifySignature(body, sig, env.WEBHOOK_SECRET);
    if (!valid) return new Response("Unauthorized", { status: 401 });

    const event = JSON.parse(body) as SocialCommentEvent;

    // Classify and reply — keep it async so we can return 200 fast
    env.REPLY_QUEUE.send(event);

    return new Response("OK", { status: 200 });
  },
};

// Queue consumer — does the actual AI work
export const queue: ExportedHandlerQueueHandler<Env, SocialCommentEvent> =
  async (batch, env) => {
    for (const msg of batch.messages) {
      const comment = msg.body;

      const classification = await classifyComment(comment.text, env);
      if (classification.intent === "spam") {
        msg.ack();
        continue;
      }

      const reply = await draftReply(comment, classification, env);

      if (classification.confidence > 0.85) {
        await postReply(comment.postId, comment.id, reply, env);
      } else {
        await flagForReview(comment, reply, env);
      }

      msg.ack();
    }
  };

Two things to notice. First, the fetch handler returns 200 immediately and offloads the real work to a queue. This keeps the webhook response fast and prevents the monitoring service from retrying. Second, the queue consumer does the actual AI call—classifying and drafting—without any time pressure from an open HTTP connection.

Scheduled: the Pickleland event promoter

Pickleland runs courts and events. Every week, someone needs to push upcoming events to the right Facebook groups to fill seats. This is pure periodic batch work—there’s no user action that triggers it, and it doesn’t need to happen in real time.

The Pickleland event promoter runs on a cron, checks the booking system for events in the next 4 days, drafts venue-specific posts for each matched Facebook group, and surfaces them for my review before anything goes live.

typescript
// workers/event-promoter.ts
export default {
  async scheduled(
    event: ScheduledEvent,
    env: Env,
    ctx: ExecutionContext
  ): Promise<void> {
    ctx.waitUntil(runPromoter(env));
  },
};

async function runPromoter(env: Env): Promise<void> {
  // Pull events from the booking system
  const upcomingEvents = await fetchUpcomingEvents(env, { daysAhead: 4 });

  if (upcomingEvents.length === 0) return;

  const drafts: PromoDraft[] = [];

  for (const event of upcomingEvents) {
    // Match each event to the right FB groups
    const groups = await matchFacebookGroups(event, env);

    for (const group of groups) {
      const post = await draftPromoPost(event, group, env);
      drafts.push({ event, group, post });
    }
  }

  // Write drafts to Airtable for review — nothing posts automatically
  await saveDraftsForReview(drafts, env);

  // Notify me via Slack
  await notifyOperator(
    `${drafts.length} promo drafts ready for review`,
    env
  );
}

The wrangler config that wires this up:

toml
# wrangler.toml
[[triggers]]
crons = ["0 18 * * 0"]  # Every Sunday at 6pm UTC

Notice what the scheduled agent can do that the event-triggered one can’t: it loops over multiple events, writes to a database, and sends a summary notification. It’s doing batch work. The event-triggered agent needs to stay lean and return fast.

Scheduled: the daily brief

Every morning at 7am my daily brief agent runs. It pulls overnight emails, my calendar, top tasks, and any news I’ve flagged as relevant. It formats everything into a single document and drops it into my AI Workspace folder.

This one is pure scheduled. There’s no event that would trigger it—I just want it every morning before I start work.

typescript
// workers/daily-brief.ts
export default {
  async scheduled(
    event: ScheduledEvent,
    env: Env,
    ctx: ExecutionContext
  ): Promise<void> {
    ctx.waitUntil(buildDailyBrief(env));
  },
};

async function buildDailyBrief(env: Env): Promise<void> {
  const [emails, calendar, tasks] = await Promise.all([
    fetchOvernightEmails(env),
    fetchTodayCalendar(env),
    fetchTopTasks(env),
  ]);

  const brief = await synthesizeBrief({ emails, calendar, tasks }, env);

  await writeToWorkspace(brief, env);
}
toml
[[triggers]]
crons = ["0 7 * * *"]  # Every day at 7am UTC

The parallel Promise.all is deliberate. Scheduled agents don’t have a human waiting—but they still shouldn’t be slower than they need to be. Pull all your data sources in parallel, then do the AI synthesis once.

When event-triggered goes wrong

The failure mode I see most often: someone builds an event-triggered agent that does too much work in the handler.

A booking comes in. The agent fetches the customer profile, enriches it from three external APIs, runs a personalization model, writes to the CRM, sends the confirmation email, and updates a dashboard. The whole thing takes 45 seconds. The booking platform retries because it didn’t get a 200 fast enough. Now the agent runs twice.

Fix it the same way the social reply agent is built: return 200 immediately, push the event to a queue, let the queue consumer do the heavy lifting asynchronously.

The other failure mode: using event-triggered for work that’s actually periodic. “Send a weekly summary” is not an event. Don’t wire it to a synthetic cron webhook—use a proper scheduled trigger.

When scheduled goes wrong

Scheduled agents fail when the job is actually latency-sensitive. If a user submits a form and the agent that processes it runs on a 5-minute cron, the user is staring at a spinner for up to 5 minutes. That’s not a scheduled job—it’s a slow event-triggered job pretending to be scheduled.

The other failure: scheduled agents that fan out to unbounded work. If your cron runs every minute and each invocation can process hundreds of records, you’ll hit Cloudflare’s CPU limits fast. Either increase the cron interval, add a queue to bound the work per invocation, or switch to Durable Objects for long-running coordination.

Mixing patterns: the booking pipeline

Some workflows genuinely need both. The Pickleland booking pipeline works like this:

  1. Event-triggered: new booking webhook → confirm the booking, send the customer a receipt, update availability. Must complete in under 10 seconds.
  2. Scheduled: every Sunday → review all bookings from the past week, generate a summary report, flag any anomalies (duplicate bookings, unusual cancellation rates).

Same domain, two patterns, two agents. The event-triggered one owns the real-time user experience. The scheduled one owns the weekly operations review. They share a database but nothing else.

Don’t try to combine them into one agent that “does everything.” You’ll end up with something that’s too slow for events and too coupled to the real-time flow for batch work.

Cloudflare Workers: why it’s the right infrastructure for both

Cloudflare Workers handles both patterns natively:

The edge deployment means your event-triggered agents respond fast globally. The free tier is generous enough to prototype both patterns without spending anything. And the unified wrangler.toml config means you’re not managing two separate infra setups for two patterns.

The one thing Workers doesn’t solve well: agents that need to run for more than a few minutes. For those, reach for Durable Objects or offload to a longer-running backend.

The operator’s bottom line

Pick your pattern before you write a single line of agent code. Event-triggered for anything a human is waiting on; scheduled for anything that runs on a clock. Keep event-triggered handlers thin—return fast, queue the work. Keep scheduled agents parallel—don’t serialize what you can parallelize. The architecture is simple. Violating it is where complexity comes from.


Related: The agent stack I use to run 30+ production agents · How I measure whether an AI agent is actually working · The cheapest way to run a content agent on Cloudflare

Want help picking the right pattern for your use case? Get in touch — I design production agent architectures for operator teams.

Keep reading

Get the AI playbook in your inbox

Every Wednesday. 28,400+ operators. Zero fluff.

↵ to see all results esc esc to close