ClawMart AI
← Back to Blog
August 27, 20268 min readClaw Mart Team

How to Schedule Cron Jobs in OpenClaw (Beginner-Friendly Examples)

How to Schedule Cron Jobs in OpenClaw (Beginner-Friendly Examples)

How to Schedule Cron Jobs in OpenClaw (Beginner-Friendly Examples)

Let's cut right to it: cron jobs are one of the most useful things you can do with an AI agent, and also one of the most frustrating to get right. The idea is simple — make your agent do something on a schedule. Check for new leads every hour. Summarize your analytics every morning. Sync inventory every fifteen minutes. The kind of stuff that, once it works, saves you hours every week.

The problem? Most people set up a cron job, walk away, and discover two weeks later that it stopped working on day three and nobody noticed. Or it ran every single time according to the logs but actually accomplished nothing. Or it quietly burned through $400 in API calls because there was no cost tracking whatsoever.

I've been there. If you've been there, this guide is for you. We're going to walk through how to schedule cron jobs in OpenClaw the right way — with real examples, proper error handling, state persistence, and the kind of observability that means you actually know what your agents are doing while you sleep.

Why Regular Cron + AI Agents Is a Terrible Experience

Before we get into how OpenClaw handles this, let me paint the picture of what "normal" looks like without it, because understanding the pain makes the solution click.

The standard approach is: write a Python script that calls an LLM, slap a cron schedule on it via crontab -e or some cloud scheduler, and hope for the best.

Here's what actually happens:

Silent failures everywhere. Your agent hits a rate limit from your LLM provider, throws an exception, and dies. Cron dutifully reports "job ran" because it did run — it just didn't do anything useful. You find out days later when you notice the reports stopped coming.

Zero observability. You get two log lines: "started" and "finished" (or "started" and nothing, which is worse). What did the agent actually do between those two lines? How many API calls did it make? How many tokens did it consume? What decisions did it make? Complete black box.

No memory between runs. Your news aggregation agent fetches the same articles every single run because it has no idea what it already processed. So you end up writing database connection code, state serialization logic, and deduplication systems — basically building half a framework just to make a cron job work properly.

Testing is miserable. Want to test your cron job? Set it to run every minute, wait, check logs, find a bug, fix it, wait again. Repeat forty times. Then remember to change it back to the real schedule before you push to prod. (You will forget.)

Cost surprises. The monthly bill arrives and it's four figures because your "simple" daily agent was actually making hundreds of GPT-4 calls per run and nobody was tracking it.

OpenClaw solves all of this. Let me show you how.

Your First Scheduled Workflow in OpenClaw

OpenClaw uses the concept of "scheduled workflows" instead of raw cron jobs. The distinction matters: a workflow is an agent execution with built-in state management, tracing, retry logic, and cost tracking. A cron job is just "run this thing at this time."

Here's the simplest possible example:

from openclaw import schedule_workflow

@schedule_workflow(cron="0 9 * * *")
async def daily_summary_agent(ctx):
    """Runs every day at 9 AM"""
    
    metrics = await ctx.fetch_data(source="analytics_db")
    summary = await ctx.agent.summarize(metrics)
    await ctx.email.send(to="team@company.com", body=summary)
    
    return {"status": "sent", "metrics_count": len(metrics)}

That's it. That @schedule_workflow decorator handles:

  • Scheduling via standard cron syntax
  • Execution logging for every step
  • Cost tracking for all LLM calls
  • Error capture with full context
  • State persistence between runs

The cron syntax is the same one you already know. 0 9 * * * means "at 9:00 AM every day." */30 * * * * means "every 30 minutes." 0 0 * * 1 means "midnight every Monday." Nothing new to learn there.

But the ctx object — that's where the magic is.

The Context Object: Your Agent's Brain

Every scheduled workflow in OpenClaw receives a context object (ctx) that gives your agent access to everything it needs. Think of it as the difference between giving someone a task on a sticky note versus giving them a task with a full briefing, a memory of everything they've done before, and a way to report back.

Here's a more realistic example — a PR monitoring agent that runs every 30 minutes:

from openclaw import schedule_workflow

@schedule_workflow(cron="*/30 * * * *")
async def pr_monitor_agent(ctx):
    """Check for new PRs and post AI-generated summaries"""
    
    # Fetch open PRs
    prs = await ctx.github.get_open_prs()
    ctx.trace("Found {count} open PRs", count=len(prs))
    
    # Get previously processed PRs from state
    already_processed = ctx.state.get("processed_pr_ids", set())
    new_prs = [pr for pr in prs if pr.id not in already_processed]
    
    ctx.trace("Processing {count} new PRs", count=len(new_prs))
    
    for pr in new_prs:
        # Generate summary — this LLM call is automatically tracked
        summary = await ctx.agent.summarize(pr.diff)
        ctx.trace("Summarized PR #{num}: {title}", 
                 num=pr.number, title=pr.title)
        
        # Post the comment
        await ctx.github.post_comment(pr.number, summary)
        already_processed.add(pr.id)
    
    # Save state for next run
    ctx.state.set("processed_pr_ids", already_processed)
    
    return {"processed": len(new_prs), "skipped": len(prs) - len(new_prs)}

Look at what's happening here. The agent remembers which PRs it already processed (ctx.state). It logs each step with context (ctx.trace). Every LLM call through ctx.agent is automatically tracked for cost and token usage. And the return value gets stored as part of the execution history.

When you open the OpenClaw dashboard after this runs, you see something like:

Run #147 — Dec 15, 2026 10:30 AM
├─ Found 6 open PRs (0.2s)
├─ Processing 2 new PRs
├─ Analyzing PR #234 "Add user authentication"
│  ├─ LLM Call: gpt-4 (2,340 tokens, $0.047)
│  ├─ Generated summary (1.8s)
│  └─ Posted comment ✓
├─ Analyzing PR #237 "Refactor payment flow"
│  ├─ LLM Call: gpt-4 (1,890 tokens, $0.038)
│  ├─ Generated summary (1.5s)
│  └─ Posted comment ✓
└─ Total: 3m 48s | Cost: $0.085 | 2 PRs processed, 4 skipped

That is light-years beyond a cron log that just says "job completed."

Handling Rate Limits and Retries (Without Losing Your Mind)

This is the one that gets people. You set up a daily report agent, it works great for a week, then one morning OpenAI's API is slow and your agent times out. No report. No notification. Just silence.

OpenClaw lets you define retry policies directly on the workflow:

from openclaw import schedule_workflow
from openclaw.retry import exponential_backoff

@schedule_workflow(
    cron="0 9 * * *",
    retry_policy=exponential_backoff(
        max_attempts=5,
        initial_delay=60,    # Wait 1 minute after first failure
        max_delay=3600       # Never wait more than 1 hour
    )
)
async def daily_report_agent(ctx):
    """Generate and send daily analytics report"""
    
    data = await ctx.fetch_metrics(source="posthog")
    
    # If this call fails due to rate limit, OpenClaw retries automatically
    report = await ctx.agent.generate_report(data)
    
    if ctx.is_retry:
        ctx.trace("This is retry attempt {n}", n=ctx.retry_count)
    
    await ctx.slack.post(channel="#daily-reports", message=report)
    
    return {"report_length": len(report)}

When a 429 (rate limit) or 503 (service unavailable) error hits, OpenClaw catches it and retries with exponential backoff. First retry after 1 minute, then 2 minutes, then 4, and so on — up to the max you set. If all retries fail, then it alerts you.

The crucial part: it continues from where it left off when possible. If your agent successfully fetched data but failed on the LLM call, the retry doesn't re-fetch the data. It picks up from the last successful step. This is possible because OpenClaw is tracking execution state throughout the workflow, not just wrapping the whole thing in a try-catch.

Here's what this looks like in practice for a content processing agent that handles large batches:

@schedule_workflow(
    cron="0 2 * * *",  # 2 AM daily
    retry_policy=exponential_backoff(max_attempts=3, initial_delay=120)
)
async def content_processor(ctx):
    """Process new articles for the content pipeline"""
    
    articles = await ctx.fetch_new_articles(limit=100)
    processed = ctx.state.get("batch_progress", [])
    
    for article in articles:
        if article.id in processed:
            continue  # Skip already-processed in case of retry
        
        summary = await ctx.agent.summarize(article)
        await ctx.database.store(article.id, summary)
        
        # Save progress incrementally
        processed.append(article.id)
        ctx.state.set("batch_progress", processed)
    
    # Clear batch progress for next run
    ctx.state.delete("batch_progress")
    
    return {"total_processed": len(articles)}

Before OpenClaw, this developer was hitting rate limits at article 23 and losing the entire batch. Now? Rate limit at article 23, automatic 2-minute backoff, resume at article 24, finish all 100. Total time goes from 15 minutes to 45 minutes — but the success rate goes from "sometimes" to 100%.

Persistent State: Stop Writing Database Code

This is one of my favorite features because it eliminates so much boilerplate. The typical cron + agent setup requires you to:

  1. Set up a database (PostgreSQL, Redis, SQLite, whatever)
  2. Write connection logic
  3. Serialize and deserialize state
  4. Handle edge cases (first run, corrupted state, migration)
  5. Write more infrastructure code than actual agent code

In OpenClaw, state just works:

@schedule_workflow(cron="0 */6 * * *")  # Every 6 hours
async def news_aggregator(ctx):
    """Aggregate news without duplicates across runs"""
    
    # Automatically loaded from previous run
    seen_article_ids = ctx.state.get("seen_articles", set())
    last_run = ctx.state.get("last_run_time")
    
    # Fetch articles since last run
    articles = await ctx.fetch_articles(since=last_run)
    new_articles = [a for a in articles if a.id not in seen_article_ids]
    
    ctx.trace("Found {new} new articles, skipping {old} already seen",
             new=len(new_articles), 
             old=len(articles) - len(new_articles))
    
    # Process only new articles
    summaries = await ctx.agent.summarize_batch(new_articles)
    await ctx.slack.post(channel="#news", message=format_digest(summaries))
    
    # Persist for next run — no database setup required
    ctx.state.update({
        "seen_articles": seen_article_ids | {a.id for a in new_articles},
        "last_run_time": ctx.timestamp,
        "lifetime_articles": ctx.state.get("lifetime_articles", 0) + len(new_articles)
    })
    
    return summaries

The ctx.state object persists automatically between runs. You don't set up anything. You don't configure a database. You just .get() and .set() and OpenClaw handles the rest. First run? The .get() calls return default values. Hundredth run? Full history available.

This is especially powerful for agents that need to learn or adapt over time:

@schedule_workflow(cron="0 * * * *")  # Hourly
async def support_agent(ctx):
    """Handle support tickets with improving context"""
    
    # Load learned patterns from all previous runs
    response_patterns = ctx.state.get("learned_patterns", {})
    success_rates = ctx.state.get("category_success_rates", {})
    
    tickets = await ctx.get_new_tickets()
    
    for ticket in tickets:
        # Use historical patterns to improve responses
        context = {
            "known_patterns": response_patterns.get(ticket.category, []),
            "historical_success_rate": success_rates.get(ticket.category, "unknown")
        }
        
        response = await ctx.agent.respond(ticket, extra_context=context)
        await ctx.post_response(ticket.id, response)
    
    # Update patterns for future runs
    ctx.state.set("learned_patterns", response_patterns)
    ctx.state.set("total_tickets_handled", 
                  ctx.state.get("total_tickets_handled", 0) + len(tickets))

Alerts That Actually Work

Here's the setup that would have saved me (and many others) from discovering failures days after the fact:

from openclaw import schedule_workflow
from openclaw.alerts import slack, email, pagerduty

@schedule_workflow(
    cron="*/15 * * * *",
    on_failure=slack("#agent-alerts"),
    on_success=email("team@company.com", only_if_changed=True)
)
async def inventory_sync(ctx):
    """Sync inventory every 15 minutes"""
    
    changes = await ctx.warehouse_api.get_changes()
    
    for change in changes:
        decision = await ctx.agent.optimize_listing(change)
        await ctx.shopify.update_product(change.sku, decision)
    
    # Alert on anomalies, not just failures
    if len(changes) > 100:
        ctx.alert.warning(f"Unusual spike: {len(changes)} inventory changes")
    
    return {"synced": len(changes)}

When this fails, you get a Slack message like:

❌ inventory_sync failed at 2:15 PM
Error: Warehouse API timeout after 30s
Last 12 runs: 11 successes, 1 failure
Retry scheduled: 2:20 PM
View details: [dashboard link]

For critical business processes, you can set up graduated alerting:

@schedule_workflow(
    cron="*/15 * * * *",
    on_failure=[
        slack("#ops", if_consecutive_failures=1),       # Slack on first failure
        slack("#ops-urgent", if_consecutive_failures=3), # Urgent channel on 3rd
        pagerduty(severity="high", if_consecutive_failures=5)  # Page someone on 5th
    ]
)

One failure? A heads-up in Slack. Three in a row? Now it's in the urgent channel. Five consecutive failures? Someone's getting paged. This is the kind of production-grade alerting that takes weeks to build from scratch.

Cost Controls: Don't Get Surprised

The $400 surprise bill story I mentioned earlier? Here's how you prevent it:

@schedule_workflow(
    cron="0 9 * * *",
    cost_limit=5.00  # Hard stop if a single run exceeds $5
)
async def daily_analysis(ctx):
    articles = await ctx.fetch_articles(limit=200)
    
    summaries = []
    for article in articles:
        # Cost is tracked in real-time per call
        summary = await ctx.agent.summarize(article, model="gpt-4")
        summaries.append(summary)
        
        # Check budget mid-execution
        if ctx.cost_so_far > 4.00:
            ctx.trace("Approaching cost limit, stopping early")
            break
    
    return {"processed": len(summaries), "cost": ctx.cost_so_far}

OpenClaw tracks cost per LLM call in real time. In the dashboard, you see cost trends over time: "This workflow cost $1.20/day last week, $1.85/day this week — 54% increase." That's the kind of visibility that prevents bill shock.

Testing Without Waiting

This one sounds small but it's a massive quality-of-life improvement. Testing cron jobs normally means waiting for the schedule to trigger. With OpenClaw:

# In development: run immediately
# $ openclaw run daily_summary_agent --now

# Or programmatically:
if __name__ == "__main__":
    from openclaw.testing import run_immediately
    
    result = await run_immediately(daily_summary_agent)
    print(f"Status: {result.status}")
    print(f"Cost: ${result.cost}")
    print(f"Output: {result.output}")

For proper test suites:

from openclaw.testing import WorkflowTest

def test_daily_summary():
    test = WorkflowTest(daily_summary_agent)
    
    # Mock external dependencies
    test.mock("fetch_data", return_value={"users": 1000, "revenue": 50000})
    test.mock("agent.summarize", return_value="Test summary")
    
    result = test.run()
    
    assert result.status == "success"
    assert result.agent_calls == 1
    assert result.cost < 1.00

You can iterate on your agent logic in seconds, not minutes. Write, test, fix, test, ship. The way development should work.

Quick Reference: Cron Syntax Cheat Sheet

Since you'll be writing cron expressions, here's a cheat sheet of common patterns:

# Every minute (good for testing, terrible for prod)
cron="* * * * *"

# Every 15 minutes
cron="*/15 * * * *"

# Every hour on the hour
cron="0 * * * *"

# Every day at 9 AM
cron="0 9 * * *"

# Every Monday at midnight
cron="0 0 * * 1"

# First day of every month at 6 AM
cron="0 6 1 * *"

# Every weekday at 8:30 AM
cron="30 8 * * 1-5"

# Every 6 hours
cron="0 */6 * * *"

# Twice a day (9 AM and 5 PM)
cron="0 9,17 * * *"

The format is: minute hour day-of-month month day-of-week. Five fields, same as traditional cron. Nothing weird or custom here.

Putting It All Together: A Production-Ready Example

Let me show you a complete, production-ready scheduled workflow that combines everything we've talked about:

from openclaw import schedule_workflow
from openclaw.retry import exponential_backoff
from openclaw.alerts import slack

@schedule_workflow(
    cron="0 8 * * 1-5",  # Every weekday at 8 AM
    retry_policy=exponential_backoff(
        max_attempts=3,
        initial_delay=300   # 5 minutes between retries
    ),
    cost_limit=10.00,
    on_failure=slack("#team-alerts"),
    on_success=slack("#daily-digest", only_if="results.lead_count > 0")
)
async def lead_enrichment_agent(ctx):
    """
    Fetch new leads from CRM, enrich with AI research, 
    and update records — every weekday morning.
    """
    
    # Load state from previous runs
    last_processed_id = ctx.state.get("last_lead_id", 0)
    enrichment_stats = ctx.state.get("weekly_stats", {
        "total_enriched": 0,
        "total_cost": 0.0,
        "week_start": ctx.timestamp
    })
    
    # Fetch new leads since last run
    new_leads = await ctx.crm.get_leads(since_id=last_processed_id)
    ctx.trace("Found {count} new leads to enrich", count=len(new_leads))
    
    enriched = []
    for lead in new_leads:
        # AI-powered research on each lead
        research = await ctx.agent.research_company(
            company=lead.company,
            context="B2B SaaS sales qualification"
        )
        
        # AI scoring
        score = await ctx.agent.qualify_lead(
            lead_data=lead,
            research=research,
            criteria=ctx.state.get("scoring_criteria", "default")
        )
        
        # Update CRM
        await ctx.crm.update_lead(lead.id, {
            "enrichment_data": research,
            "ai_score": score,
            "enriched_at": ctx.timestamp
        })
        
        enriched.append({"id": lead.id, "score": score})
        ctx.trace("Enriched lead {id}: score {score}", 
                 id=lead.id, score=score)
    
    # Update persistent state
    if new_leads:
        ctx.state.set("last_lead_id", new_leads[-1].id)
    
    enrichment_stats["total_enriched"] += len(enriched)
    enrichment_stats["total_cost"] += ctx.cost_so_far
    ctx.state.set("weekly_stats", enrichment_stats)
    
    return {
        "lead_count": len(enriched),
        "high_score_leads": [l for l in enriched if l["score"] > 80],
        "run_cost": ctx.cost_so_far
    }

This agent runs every weekday at 8 AM, enriches new CRM leads with AI research, scores them, updates the CRM, tracks costs, retries on failure, sends Slack alerts, and maintains state across runs. And it's readable. You can look at this and understand exactly what's happening.

Skip the Setup: Get Running in Minutes

If you've read this far, you're probably either excited to build this or dreading the initial setup. I get it — even with OpenClaw making the actual workflow code clean, there's still configuration, skill setup, and the initial "figuring out how everything connects" phase.

Here's my honest recommendation: if you don't want to set all this up manually, Felix's OpenClaw Starter Pack on Claw Mart is worth the $29. It includes pre-configured skills for exactly the kind of scheduled workflows we've been talking about — state management, retry logic, alerting, cost tracking — all wired up and ready to go. I spent a full weekend configuring my first production workflow from scratch; this bundle would have saved me that time. It's not magic, but it's a really solid starting point that you can customize from there.

What to Build First

If you're new to scheduled workflows in OpenClaw, here's my suggested progression:

  1. Start simple. A daily summary agent that fetches data from one source and posts to Slack. Get comfortable with @schedule_workflow and ctx.

  2. Add state. Modify your agent to remember what it already processed. Use ctx.state to avoid duplicate work.

  3. Add error handling. Set up retry policies and Slack alerts. Break your agent on purpose and make sure the alerts work.

  4. Add cost controls. Set a cost_limit and watch the dashboard for a week. You'll be surprised what you learn about your agent's actual spending patterns.

  5. Go multi-step. Build a workflow with 3+ stages that depend on each other. This is where OpenClaw's tracing really shines.

The goal is to get to a place where your agents run reliably in the background, you get notified when something goes wrong, and you have full visibility into what they're doing and what they're costing you. That's the dream, and it's very achievable with OpenClaw's scheduling system.

Stop babysitting your cron jobs. Set them up right once, and let them work.

Recommended for this post

Your AI runs its own Twitter account — posting, replying, and engaging with guardrails.

All platformsContent96 sold
Felix CraftFelix Craft
$9Buy

Claw Mart Daily

Get one AI agent tip every morning

Free daily tips to make your OpenClaw agent smarter. No spam, unsubscribe anytime.

More From the Blog