Recurring Jobs
Run custom Python code on a recurring schedule — daily reports, periodic checks against external systems, scheduled cleanups. No external hosting required.
Recurring jobs let you run Python code automatically on a recurring schedule. They complete the set of Python triggers in Octocom: custom actions run when the AI calls them during a conversation, event handlers run when conversation events fire, and recurring jobs run on a schedule — no conversation required.
Whenever something in Octocom should happen on a regular basis, a recurring job means you don't need to host the recurrence anywhere else — no external cron server, no serverless function to maintain.
Common uses:
- Send a daily summary or digest to Slack or email
- Periodically reconcile data with an external system (orders, subscriptions, inventory)
- Run scheduled sweeps or cleanups
- Post regular reports to a Google Sheet
How it works
- You create a recurring job — give it a name, pick a business, set a schedule, and write your Python code
- The job runs automatically at each scheduled occurrence
- Every run is recorded — result, stdout, stderr, and failure reason — and shown in the job's run history
A recurring job belongs to your organization and runs in the context of one business (available to your code as context["business"]).
Schedules
A schedule is a standard 5-field cron expression (minute hour day-of-month month day-of-week), evaluated in the job's timezone:
| Expression | Meaning |
|---|---|
0 9 * * * | Daily at 09:00 |
*/15 * * * * | Every 15 minutes |
0 9 * * 1 | Mondays at 09:00 |
30 7 1 * * | The 1st of every month at 07:30 |
0 8,17 * * * | Twice a day, at 08:00 and at 17:00 |
The timezone is an IANA name like UTC, Europe/Vilnius, or America/New_York, so "daily at 9am" stays at 9am local time across daylight-saving changes.
Two limits to be aware of:
- Minimum interval: 5 minutes. Schedules whose occurrences are closer together than 5 minutes are rejected when you save.
- Runtime limit: 60 seconds per run. Keep jobs focused; offload heavy processing to external systems if needed.
Invalid cron expressions and unknown timezones are rejected when you save the job, with an error explaining what's wrong.
Writing a recurring job
A recurring job implements a run_job function. It receives the same kind of context object as custom actions, with one difference: there is no conversation, so context["conversation"] and context["customer"] are None.
import requests
def run_job(context):
business = context["business"]
# Fetch something from your own API and act on it
response = requests.get(
"https://api.example.com/pending-orders",
timeout=30,
)
pending = response.json()
return {"business": business["slug"], "pending": len(pending)}The job descriptor
context["job"] describes the run:
| Field | Description |
|---|---|
id | The recurring job's ID |
name | The recurring job's name |
schedule | The cron expression |
timezone | The schedule's timezone |
scheduled_for | The occurrence this run is for (ISO timestamp; None for manual runs) |
last_run_at | When the job last ran (None on the first run) |
trigger | "schedule" for scheduled runs, "manual" for manually triggered runs |
See Python Context for the full context reference.
Helper functions
Recurring jobs have access to the same built-in helper functions as custom actions and event handlers. Helpers that operate on a conversation need to be given a conversation explicitly, since a recurring job doesn't run inside one.
See Python Helpers for the full list.
Example: daily Slack digest
import requests
def run_job(context):
business = context["business"]
requests.post("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", json={
"text": f"Good morning! Daily digest for {business['name']} is ready.",
}, timeout=10)
return {"status": "sent"}Schedule: 0 9 * * *, timezone Europe/Vilnius.
Example: weekly Google Sheets report
from datetime import datetime, timezone
def run_job(context):
add_google_sheets_row("YOUR_SPREADSHEET_ID", {
"Week of": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"Business": context["business"]["slug"],
"Status": "ok",
})
return {"status": "logged"}Schedule: 0 8 * * 1 (Mondays at 08:00).
Testing and run history
You can test a recurring job's code from the dashboard before it goes live:
- Open the recurring job in the dashboard
- Click Run Test in the Test Job panel
- The code runs once, immediately, and shows the result, stdout, and stderr
Test runs are dry runs — they are not recorded in the run history.
Every real run (scheduled or manually triggered) is recorded. The Run History panel on the job's edit page shows recent runs with their status, duration, output, and failure reason, so you can always answer "what did this job do last Tuesday" without digging through logs.
Reliability behavior
- Late pickup skips stale occurrences. If the system picks a job up late, it runs once and then waits for the next future occurrence — it never fires a backlog of missed runs in a burst. Occurrences stay aligned to the cron grid; use
context["job"]["scheduled_for"]if your code needs to know which occurrence it's running for. - Repeated failures pause the job. If a job fails many times in a row, it is automatically deactivated so a broken script doesn't keep running indefinitely. Editing the job (or re-enabling it) resets the failure counter.
- Active job limit. An organization can have up to 25 active recurring jobs.
Best practices
- Be idempotent. Design jobs so that running twice for the same period produces the same result — a late pickup or a manual run alongside a scheduled one shouldn't double-post or double-charge anything.
- Don't assume every occurrence ran. If your job processes "everything since the last run", derive the window from your own data (e.g. a "last processed" marker in the external system) rather than from the schedule.
- Handle errors gracefully. If an external API is down, catch the exception and return an error status. Failures are recorded in the run history either way, but a clean error message is easier to debug — and uncaught crashes count toward automatic deactivation.
- Keep runs under the 60-second limit. Batch or paginate work; if one run can't finish the whole task, make the job resumable so the next occurrence continues where it left off.
- Watch the run history after changes. After creating or editing a job, check its next run's output in the Run History panel to confirm it behaves as expected.