Skip to content

Configure

Background jobs

Scheduled workflows, polling triggers, and the cron endpoints that drive them

Scheduled workflows, polling triggers, connector syncs, and maintenance jobs depend on authenticated HTTP endpoints called by an external scheduler.

Helm enables CronJobs by default; Docker Compose includes a cron service. Both use CRON_SECRET and the same shipped schedules. Upload-session cleanup needs an additional operator-configured job, described below.

Authentication

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

Code
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:

Code
openssl rand -hex 32

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.

Kubernetes

CronJobs are enabled by default. Supply CRON_SECRET through the app Secret and ensure cron pods can reach the internal app Service.

Code
cronjobs:
  enabled: true

Each curlimages/curl pod calls the internal app Service, which routes the request to one replica. concurrencyPolicy: Forbid prevents overlapping Jobs for that CronJob; the app also handles overlap for detached work.

SettingShipped behavior
Completed/failed Job historyThree successful and one failed Job
Finished-Job TTL600 seconds; collect logs before deletion
Job deadline300 seconds by default; outbox uses 900, idempotency cleanup 360
Kubernetes retry limitNo chart backoffLimit key; Kubernetes defaults to six retries after the initial failure
Request retriesThree shell attempts, ten seconds apart; each uses curl --retry 2 --retry-delay 5
Request timeout60 seconds by default, with per-job overrides; outbox uses 810 seconds

The Job deadline includes attempts and controller backoff. It can end a Job before the retry limit is reached and also stops a hung request.

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

Code
cronjobs:
  jobs:
    reconcileBillingSeats:
      enabled: false

Do not disable schedule execution, outbox processing, or subscription renewal unless you know you do not use the features behind them.

Check they are running:

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

Inspect stale schedules and failed Jobs. A stale LAST SCHEDULE points to controller, suspension, or deadline settings; a 401 in Job logs points to authentication. Forbid can skip a tick while an earlier Job remains active. Alert on successful completion as described in Observability.

Docker Compose

Generate a secret, then save it as CRON_SECRET in the .env file next to your Compose file. Replace an existing value rather than adding a duplicate. The app and scheduler must receive the same value.

Code
openssl rand -hex 32

Recreate the services after updating .env:

Code
docker compose -f docker-compose.prod.yml up -d

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.

An older install may lack CRON_SECRET; add it to .env and run up -d to enable the scheduler.

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.

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

A healthy log line looks like:

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

The published cron image contains its crontab. Editing the repository copy and restarting that image does not change its jobs. To customize them, copy docker/crontab to an operator-owned file, edit it, and mount it with a Compose override:

Code
# cron-custom.yml
services:
  cron:
    volumes:
      - ./sim-crontab:/etc/sim/crontab:ro
Code
docker compose -f docker-compose.prod.yml -f cron-custom.yml up -d cron

Alternatively, build a custom image from docker/cron.Dockerfile with your crontab and configure Compose to use it.

Schedule upload-session cleanup

The cleanup endpoint exists, but neither shipped scheduler calls it. Configure your own authenticated schedule for /api/cron/cleanup-tasks to expire upload sessions and reclaim abandoned upload state. It also dispatches task-retention cleanup according to configured policy.

For Helm, add an operator-owned job to your values file. This example runs hourly; choose a cadence and timeout for your workload:

Code
cronjobs:
  jobs:
    uploadSessionCleanup:
      enabled: true
      name: upload-session-cleanup
      schedule: "0 * * * *"
      path: /api/cron/cleanup-tasks
      requestTimeoutSeconds: 300
      activeDeadlineSeconds: 360
      concurrencyPolicy: Forbid

For Compose, add this line to the mounted or custom-image crontab described above:

Code
0 * * * * curl -fsS -m 300 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-tasks"

Use the internal app address and the same CRON_SECRET. Check job logs for a successful response. Provider lifecycle rules remain a second layer for incomplete multipart state; see Object storage.

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 -f docker-compose.prod.yml 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.

Shipped job reference

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
Workspace file search dispatch/api/cron/workspace-file-search-dispatch*/1 * * * *Dispatches indexing work for workspace file search
Knowledge projection/api/cron/knowledge-projection*/1 * * * *Brings knowledge base search up to date with document, permission, and chunk changes
Table row-change fold/api/cron/fold-table-row-changes*/1 * * * *Folds the table row-change log into each table's stored row count and version
Connector sync/api/knowledge/connectors/sync*/5 * * * *Knowledge base connector syncs
Connector member sync/api/knowledge/connectors/member-sync*/5 * * * *Per-member content/access sync for indexed connectors; live Search sources are skipped
Connector directory sync/api/knowledge/connectors/directory-sync*/5 * * * *Refreshes stored groups for indexed administrator-mode connectors; live Search sources are skipped
Workspace events poll/api/workspace-events/poll*/15 * * * *Workspace event triggers
Table row TTL cleanup/api/cron/cleanup-table-row-ttl*/15 * * * *Deletes table rows whose TTL column has expired
Stale execution cleanup/api/cron/cleanup-stale-executions*/30 * * * *Fails runs whose worker died without finishing — workflow executions, background jobs, table jobs and table runs — so they stop showing as running
OAuth token cleanup/api/cron/cleanup-oauth-tokens0 * * * *Deletes access and refresh tokens after the retention tail
SCIM reconciliation/api/cron/scim-reconcile17 * * * *Re-applies directory group mappings
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)
Billing cycle close/api/cron/billing-cycle-close0 */6 * * *Billing only — final overage collection and per-period tracker reset
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
Cleanup session sandboxes/api/cron/cleanup-session-sandboxes*/5 * * * *Kills chat workbenches paused past their retention window
Idempotency key cleanup/api/webhooks/cleanup/idempotency0 5 * * *Deletes deduplication records older than seven days

Subscription renewal covers Microsoft Teams chat triggers. Their Graph subscriptions last at most three days; rich notifications have a shorter limit. Gmail, Outlook, Drive, Calendar, and Sheets use the per-minute polling jobs instead. See Microsoft Graph subscription lifetimes. SCIM reconciliation calls its authenticated endpoint directly each hour; it does not require Trigger.dev.

Concurrency

On the default database-backed queue, scheduled execution capacity is shared across the deployment:

VariableDefaultApplies to
SCHEDULE_EXECUTION_CONCURRENCY_LIMIT30Scheduled workflows in flight across the database-backed queue; also the schedule task limit on Trigger.dev

Adding replicas does not multiply the database queue's schedule cap. Raise it only after measuring memory and database headroom. The app orchestrates runs; local JavaScript uses Node child workers inside its container.

Was this helpful?
Suggest an edit