Phase 03 — Production
Module 09 of 12 · 8 min read · Free

Module 9: Scheduling and Background Work

The shift from tool to autonomous assistant happens the moment your agent starts doing things without being asked.

This is Module 9 of a 12-part curriculum: Build Software Products with AI — From First Principles to Production Pipeline.


Everything we’ve covered so far has been reactive: a user sends a message, an agent responds. This is the chatbot model. It’s useful, but it’s the floor — not the ceiling.

The most powerful version of an agent is one that operates proactively. It checks your inbox before you ask. It processes meeting transcripts when they arrive. It backs up your work at midnight. It reminds you about the meeting in 20 minutes.

That shift — from reactive to proactive — is what this module is about.


Push vs Pull

There are two fundamental models for when an agent takes action.

Pull (reactive). The agent waits for input. User sends a message → agent processes it → agent responds. The user is always the initiator.

Push (proactive). The agent initiates action based on time, events, or conditions. A schedule fires → agent checks the inbox. A file appears in a directory → agent processes it. A threshold is crossed → agent sends an alert.

Most agent setups start with pull. The leap to push is what makes an agent feel like an assistant rather than a tool.

SCHEDULING MODELS — pull vs push
PULL  (reactive)                         PUSH  (proactive)
─────────────────                         ─────────────────
human asks     →  agent responds          schedule fires  →  agent acts
no input → no work                       file appears    →  agent processes
                                          threshold hit   →  agent alerts

example: /ask command example: cron job at 09:00 runs once on request runs daily, no prompt needed heartbeat poll every 30 min event watcher on drop folder

feels like: a tool 🔧 feels like: an assistant 🤖

The architectural shift: pull agents wait. Push agents act.


Cron Jobs

Cron is the simplest form of scheduled execution. At a defined time or interval, run a task.

# Runs every weekday at 8am London time
0 8 * * 1-5  → run morning briefing agent

# Runs every day at 11pm
0 23 * * *   → run action items cleanup

# Runs every Sunday at 8pm
0 20 * * 0   → run weekly content planning session

Cron jobs are suited for:

  • Tasks with exact timing requirements (8:00 AM sharp, not “sometime in the morning”)
  • Tasks that run independently of the main conversation
  • Tasks that produce output delivered to a channel without main session involvement
  • Long-running background work

Each cron job runs in its own isolated session. It has its own context, its own model selection, and its own output routing. It doesn’t inherit the state of your main session.

Design principle: cron jobs should be self-contained. A cron job that depends on state set in a previous conversation is fragile. It should read what it needs from files, do its work, and write its output.


Heartbeats

A heartbeat is a periodic check that runs within the main session. Rather than an isolated cron job, it’s a regular nudge: “while things are quiet, go check if there’s anything worth doing.”

Heartbeats are suited for:

  • Batching multiple lightweight checks together (inbox + calendar + notifications in one turn)
  • Tasks where you want the main session’s context available
  • Checks where timing can drift slightly (every 30 minutes is fine, not exact)
  • Proactive work the assistant can do during quiet periods

A HEARTBEAT.md file defines what to check on each heartbeat:

# HEARTBEAT.md

## Check for new meeting transcripts
Scan the drop folder for unprocessed transcript files.
If found, run the meeting-scribe pipeline.

## Triage shared inbox
Check Shared Inbox.md for unprocessed items.
Add brief descriptions. Note project relevance. 
Mark processed items with ✅.

## Sync task backlog
Run sync-context-files.js. Report any status changes.

## Check backlog for ready items
If any backlog items in "ready" column, alert.

The agent reads this on each heartbeat and works through it. Small nudges — a few checks, a few updates — that compound into a system that feels alive and attentive.


One-Shot Reminders

Beyond scheduled jobs and heartbeats, there’s a simpler pattern: set a reminder for a specific moment.

“Remind me in 20 minutes about the call.” “Alert me at 3pm if the CI hasn’t passed yet.” “Check in tomorrow morning about whether I’ve sent that email.”

These are one-shot cron entries — they fire once and expire. In practice, they’re short-lived timer tasks that the main agent creates and monitors.

Implementation: write a cron entry to fire at the specified time, with a message to deliver to the user. The cron job fires, delivers the message, and is removed.


Event-Driven Triggers

Beyond time-based scheduling, agents can react to events: something happened, therefore act.

Common event patterns:

File system watchers. A new transcript file appears in the drop folder → process it. A new video lands in the render queue → start transcoding. A config file changes → reload and apply.

Webhook receivers. A GitHub webhook fires when a PR is opened → run automated review. A Stripe webhook fires when a payment succeeds → update user tier. A calendar event starts → send a prep brief.

Database triggers. A record is created → kick off a workflow. A status changes → notify the relevant agent.

Message patterns. A message contains a specific keyword or hashtag → route it. A message arrives in a specific channel → auto-process.

Event-driven triggers are more responsive than polling. Instead of checking “is there a new file?” every 5 minutes, you’re notified the moment a file arrives. For time-sensitive workflows, this matters.


State Tracking for Background Work

Background agents need to track their own state. Without it, they lose track of what they’ve processed, what they’ve skipped, and where they left off.

For heartbeat-driven checks, maintain a heartbeat-state.json:

{
  "lastChecks": {
    "email": 1748423400,
    "calendar": 1748423400,
    "transcripts": 1748420000
  },
  "processedFiles": [
    "2026-05-09-standup.md",
    "2026-05-08-roadmap-review.md"
  ],
  "backlog": {
    "inProgress": ["TASK-042", "TASK-047"]
  }
}

The agent reads this state at the start of each check, uses it to decide what’s new, and writes back after processing. This prevents the same email being flagged twice, the same transcript being processed twice, or the same backlog item being picked up twice.

For cron jobs: maintain a processed log. A simple text file or JSON array of IDs that have been handled. On each run, check the log before processing anything.


The Overnight Agent

One of the most practical applications of background scheduling: agents that do work while you sleep.

My nightly schedule:

  • 23:00 — Clean up resolved action items from the Obsidian vault
  • 23:30 — Back up the workspace to GitHub
  • 01:00 — Generate tomorrow’s daily note and priorities

By 8:00 AM when I open my laptop, these things are already done. The daily note is waiting. The workspace is backed up. Resolved tasks are cleaned up.

None of this requires intelligence — it requires reliable scheduling and clear task definitions. The value isn’t the individual tasks. It’s the accumulation of automated housekeeping that would otherwise drain small amounts of attention every day.


Proactive Communication

A proactive agent doesn’t just do tasks silently — it communicates when something warrants attention.

Rules for when to surface a proactive notification:

  • An urgent email arrived that requires action
  • A calendar event is within 2 hours
  • A task moved to “blocked” and needs unblocking
  • A CI build failed on a branch you care about
  • Something genuinely new and relevant happened

Rules for when NOT to:

  • It’s outside working hours (unless truly urgent)
  • Nothing new has happened since the last check
  • The check ran less than 30 minutes ago
  • It’s a routine update that can wait until you check in

The goal is being attentive without being annoying. An assistant that pings you for every minor status update is worse than no assistant at all. Be selective. Surface the signal, suppress the noise.


Designing for Reliability

Background jobs fail. Network is unavailable. Files are malformed. APIs return errors. Build your background work with failure in mind.

Idempotency. Running the same job twice should produce the same result as running it once. If a transcript was already processed, processing it again should be a no-op, not a duplicate.

Error logging. When a background job fails, log it. Don’t fail silently. Write the error to a log file so you can diagnose it later.

Graceful degradation. If one check fails (e.g., email API is down), continue with the other checks. Don’t let one failure abort the whole heartbeat.

Alerting on repeated failures. If the same job fails three times in a row, surface it. Silent repeated failures are the worst kind.


What’s Next

Your agent can now act proactively — on a schedule, in response to events, and during quiet periods. In Module 10, we make it production-ready: failure handling, retry logic, fallback models, approval gates, and the security and safety architecture that makes a system you can trust.


Further Reading

Referenced from @nikovijay

“This is my Mission Control: A Squad of 10 autonomous @openclaw agents… They create work on their own.” — @pbteja1998

N+1 Newsletter
Enjoyed this module?

Subscribe to get notified when new modules and courses drop. No drip — just updates when there's something worth reading.

Subscribe on Substack →