Skip to content

Scheduled Workflows

Run workflows on a schedule using cron expressions

Schedule workflows to run automatically using cron expressions.

Add a schedule parameter to your workflow decorator:

from bifrost import workflow
@workflow(schedule="0 9 * * *")
async def daily_report():
"""Generate daily report at 9 AM UTC."""
return {"status": "report_sent"}

Standard 5-field cron format: minute hour day month weekday

FieldValuesDescription
Minute0-59Minute of the hour
Hour0-23Hour of the day (UTC)
Day1-31Day of the month
Month1-12Month of the year
Weekday0-6Day of week (0=Sunday)
# Every hour at minute 0
@workflow(schedule="0 * * * *")
# Every day at 9 AM UTC
@workflow(schedule="0 9 * * *")
# Every Monday at 8 AM UTC
@workflow(schedule="0 8 * * 1")
# First day of every month at midnight
@workflow(schedule="0 0 1 * *")
# Every 15 minutes
@workflow(schedule="*/15 * * * *")
# Weekdays at 6 PM UTC
@workflow(schedule="0 18 * * 1-5")
from bifrost import workflow, config
import httpx
import logging
logger = logging.getLogger(__name__)
@workflow(
schedule="0 8 * * 1-5", # Weekdays at 8 AM UTC
timeout_seconds=300,
category="Reports"
)
async def send_daily_summary():
"""Send daily summary email to team."""
logger.info("Starting daily summary")
# Gather metrics
metrics = await gather_metrics()
# Send notification
webhook_url = await config.get("slack_webhook")
async with httpx.AsyncClient() as client:
await client.post(webhook_url, json={
"text": f"Daily Summary: {metrics['total_tasks']} tasks completed"
})
return {"sent": True, "metrics": metrics}
  1. Discovery: Scheduler finds workflows with schedule parameter
  2. Registration: Jobs are registered with next run time
  3. Execution: At scheduled time, workflow runs in global scope
  4. Logging: Results stored in execution history

Scheduled workflows have limited context:

from bifrost import workflow, context
@workflow(schedule="0 0 * * *")
async def scheduled_task():
# context.org_id is None (global scope)
# context.user_id is "system" or scheduler user
# To process all orgs:
for org in await get_all_orgs():
await process_org(org.id)
  • View scheduled jobs in Workflows → filter by scheduled
  • Execution history shows scheduled runs
  • Failed runs appear in execution logs

Remove or comment the schedule parameter:

# Temporarily disabled
@workflow(
# schedule="0 9 * * *", # Disabled
category="Reports"
)
async def daily_report():
...

Or redeploy without the schedule.

  • Use UTC: All schedules are in UTC timezone
  • Avoid overlaps: Don’t schedule faster than execution time
  • Set timeouts: Prevent long-running scheduled jobs
  • Log progress: Track execution for debugging