Background Jobs

A large part of Sim runs on a schedule rather than in response to a user request: scheduled workflows, every polling trigger, connector syncs, the outbox, data drains, and retention. All of it is driven by HTTP endpoints that something external must call on a timer.

Both deployments ship a scheduler and enable it by default: Kubernetes as CronJobs, Docker Compose as a cron service. Both authenticate with CRON_SECRET, and both use the same schedules.

Authentication

Every endpoint is protected by CRON_SECRET and expects it as a bearer token:

curl -f -s -S --max-time 60 \
  -H "Authorization: Bearer $CRON_SECRET" \
  https://sim.yourdomain.com/api/schedules/execute

Generate it like the other secrets:

openssl rand -hex 32

CRON_SECRET is required whenever background jobs are enabled — which is the Helm chart's default. The chart refuses to render without it. If it is unset, the endpoints reject every call and all scheduled work silently stops.

Point cron at an internal address where possible (the in-cluster Service, or localhost on a single node). These endpoints should not be reachable from the internet; if they are, CRON_SECRET is the only thing protecting them.

The jobs

JobEndpointScheduleDrives
Schedule execution/api/schedules/execute*/1 * * * *Scheduled workflows
Gmail poll/api/webhooks/poll/gmail*/1 * * * *Gmail trigger
Outlook poll/api/webhooks/poll/outlook*/1 * * * *Outlook trigger
IMAP poll/api/webhooks/poll/imap*/1 * * * *IMAP trigger
RSS poll/api/webhooks/poll/rss*/1 * * * *RSS trigger
Google Sheets poll/api/webhooks/poll/google-sheets*/1 * * * *Sheets trigger
Google Drive poll/api/webhooks/poll/google-drive*/1 * * * *Drive trigger
Google Calendar poll/api/webhooks/poll/google-calendar*/1 * * * *Calendar trigger
HubSpot poll/api/webhooks/poll/hubspot*/1 * * * *HubSpot trigger
Time pause/resume/api/resume/poll*/1 * * * *Workflows paused on a timer
Outbox processing/api/webhooks/outbox/process*/1 * * * *Transactional-outbox retries for billing, membership, enterprise issuance, and workflow-deployment side effects
Connector sync/api/knowledge/connectors/sync*/5 * * * *Knowledge base connector syncs
Workspace events poll/api/workspace-events/poll*/15 * * * *Workspace event triggers
Data drains/api/cron/run-data-drains0 * * * *Enterprise data drains
Renew subscriptions/api/cron/renew-subscriptions0 */12 * * *Renews Microsoft Teams chat subscriptions (Graph caps them at ~3 days)
Reconcile billing seats/api/cron/reconcile-billing-seats0 * * * *Billing only — safe to disable when self-hosted
Reconcile inbox entitlement/api/cron/reconcile-inbox-entitlement0 3 * * *Inbox access reconciliation
Cleanup sandbox images/api/cron/cleanup-sandbox-images30 4 * * *Reclaims sandbox images

Subscription renewal covers Microsoft Teams chat triggers, whose Microsoft Graph subscriptions are hard-capped at about three days. Without it, Teams triggers work for a couple of days and then quietly stop. Gmail, Outlook, Drive, Calendar, and Sheets triggers are polled instead — they depend on the per-minute poll jobs above, not on this one.

Kubernetes

Enabled by default. Nothing to do beyond setting CRON_SECRET.

cronjobs:
  enabled: true

Each job runs a small curlimages/curl pod that calls the app's in-cluster Service (not the ingress), with concurrencyPolicy: Forbid so a slow run never overlaps itself, and up to three retries.

Disable individual jobs you do not need — billing reconciliation is the obvious one on a self-hosted install:

cronjobs:
  jobs:
    reconcileBillingSeats:
      enabled: false

Check they are running:

kubectl get cronjobs -n simstudio
kubectl get jobs -n simstudio --sort-by=.metadata.creationTimestamp | tail
kubectl logs -n simstudio job/<job-name>

A CronJob whose LAST SCHEDULE is stale, or whose jobs are failing, means the corresponding feature is dead. Alert on it — see Observability.

Docker Compose

The cron service runs the same jobs on the same schedules, so nothing to configure beyond CRON_SECRET:

CRON_SECRET=$(openssl rand -hex 32)

Without it the cron service logs exactly what to set — including a freshly generated value — and exits, leaving the rest of the stack running. Schedules live in docker/crontab and mirror helm/sim/values.yaml cronjobs.jobs one-for-one.

Upgrading a deployment created before the scheduler existed? Your .env has no CRON_SECRET, so the stack comes up as before and cron exits with instructions. Add the value and re-run up -d to turn background jobs on.

The service runs supercronic rather than the app image: it logs each job's output to the container log, forwards SIGTERM so docker compose stop is graceful, and will not start an iteration while the previous one is still running.

docker compose -f docker-compose.prod.yml logs -f cron

A healthy log line looks like:

level=info msg=starting iteration=0 job.schedule="*/1 * * * *"
level=info msg="job succeeded" iteration=0

To drop a job you do not need, comment out its line in docker/crontab and restart the service.

Verifying

Create a workflow with a Schedule trigger set to every minute, deploy it, and watch the Logs view. An execution should appear within ~2 minutes. If nothing appears:

  1. Check the scheduler's own logs — docker compose logs cron, or kubectl get cronjobs -n simstudio for a recent LAST SCHEDULE.
  2. Confirm the app and the scheduler share the same CRON_SECRET. A mismatch shows up as 401 in the scheduler log.
  3. A 202 means the endpoint accepted the run; it does not confirm a schedule was due, so check the Logs view.

Concurrency

Scheduled execution volume is bounded per app instance by:

VariableDefaultApplies to
SCHEDULE_EXECUTION_CONCURRENCY_LIMIT30Scheduled workflows in flight, on every install

WORKFLOW_EXECUTION_CONCURRENCY_LIMIT, WEBHOOK_EXECUTION_CONCURRENCY_LIMIT, and RESUME_EXECUTION_CONCURRENCY_LIMIT are concurrencyLimit settings on Trigger.dev task definitions. They have no effect unless TRIGGER_DEV_ENABLED is set, and neither the Helm chart nor Docker Compose configures Trigger.dev — so on a default self-host they are inert.

Raise the schedule limit only alongside memory headroom: concurrent executions run in the app process, so throughput is bounded by the pod's memory before it is bounded by this number.

Common Questions

Yes. The cron service runs the same 18 jobs on the same schedules the Helm chart uses, from docker/crontab. It needs CRON_SECRET; without it the service prints what to set and exits while the rest of the stack keeps running.
No. Point cron at the in-cluster Service or localhost. If the endpoints are internet-reachable, CRON_SECRET is the only control protecting them.
Yes — set cronjobs.jobs.<name>.enabled: false in Helm, or omit the crontab line. Billing-seat reconciliation is the usual candidate on a self-hosted install. Do not disable schedule execution, outbox processing, or subscription renewal unless you know you do not use the corresponding features.
No. Each CronJob makes one HTTP call to the app Service, which load-balances to a single replica. concurrencyPolicy: Forbid prevents a slow run from overlapping the next tick.

On this page