Observability

Background jobs

Exports, imports and bulk actions run in the background: the Jobs panel, completion popups, async API requests, CLI waiting, limits and retention.

Slow actions — exports, imports, package builds and installs, bulk deletes and other actions on many items, sync runs — run as background jobs. Clicking the button returns at once with “Preparing your export… we'll let you know when it's ready”; you can keep working. Quick jobs still finish within the click.

In the app

  • Jobs indicator (top bar): a spinner and the number of running jobs. Click it for the Jobs panel: recent jobs with their progress, Cancel, Retry and Download. Workspace admins (Edit settings) can switch to every job of the workspace.
  • Completion popup: when a job finishes — on whatever page you are by then — a popup offers Download (exports), shows the result (imports, bulk actions) or the error with Retry.
  • Bell: every finished job also leaves a notification, so a result is not lost when you navigate away or reload. Clicking it opens the Jobs panel.

Results (files) are kept for 7 days, then deleted with the job. Only the person who started a job can download its file.

ActionJob kind
Export (any artifact or selection)config.export
Import (apply; the dry-run plan is instant)config.apply
Report → Download Excelreport.export
Packages: build & download, download a version, install / upgrade, complete setup, uninstall, history CSVpackage.*
Delete selected (workflows, data tables, rows, business rules, agents, reports, workflow activities, retention policies, references, API keys)bulk.delete
Activate / deactivate selected workflowsworkflows.set_active
Bulk permissionsobject_permissions.bulk_set
Add tag to selected referencesreferences.add_tag
Bulk assign / remove a roleroles.bulk
Regenerate connector referencesconnectors.regenerate_references
Queue: Pause all / Resume all / Deleteworkflow_queue.bulk
Migrate live instancesworkflow_instances.migrate
GitHub sync pushes and deployments, directory syncssync.watch

Permissions are the same as for the action itself, and are checked again — as you, with your roles at that moment — when the job runs.

API

Slow endpoints have an async variant. Add ?async=true or the header Prefer: respond-async; the response is 202 Accepted with the job (and Location: /api/v1/jobs/{id}):

bash
curl -X POST "$URL/api/v1/config/apply" -H "Authorization: Bearer $KEY" \
  -H "Prefer: respond-async" -H "Content-Type: application/json" -d @files.json
# 202 { "success": true, "data": { "job": { "id": "…", "status": "queued", … } } }

curl "$URL/api/v1/jobs/$JOB" -H "Authorization: Bearer $KEY"          # status, progress
curl "$URL/api/v1/jobs/$JOB/result" -H "Authorization: Bearer $KEY"   # what the synchronous call returns
curl -OJ "$URL/api/v1/jobs/$JOB/download" -H "Authorization: Bearer $KEY"   # file results

Async endpoints: GET /config/export, POST /config/apply, POST /packages/{id}/versions, POST /package-installs, POST /package-installs/{id}/upgrade, POST /package-installs/{id}/uninstall, POST /data-tables/{id}/rows/bulk-delete, POST /references/bulk-delete, POST /workflows/{id}/instances/migrate (marked x-usertasks-async in the OpenAPI document). An Idempotency-Key header returns the same job for a repeated request. Without the flag every endpoint answers synchronously, as before.

Endpoint
GET /jobsYour jobs (tenant API keys: the jobs the key started); scope=tenant for admins, active=true, status=…
GET /jobs/{id}Status (queued, running, succeeded, failed, cancelled), progress 0–100, message, result summary and counts, error
POST /jobs/{id}/cancelQueued jobs stop at once; running jobs at their next progress report
POST /jobs/{id}/retryA new job with the same input
GET /jobs/{id}/resultThe JSON result (409 until finished, 410 once expired)
GET /jobs/{id}/downloadThe result file

A job started with an API key keeps that key's permissions as a ceiling and stops if the key is revoked or expires.

SDK and CLI

ts
const result = await client.requestAndWait('POST', '/config/apply', { body, onProgress: (job) => console.log(job.progress) })
const { job } = await client.startJob('GET', '/config/export', { query: { format: 'manifest' } })
const file = await client.downloadJob(job.id)

CLI commands with an async variant start a job and wait for it, printing progress on stderr and the same output as before; --no-wait prints the job instead. usertasks jobs list | get | cancel | retry | result | wait | download work with any job.

How it runs (self-hosting)

  • The job starts in the same request, right after the response (after()), so it needs no extra infrastructure.
  • The scheduler tick runs user-jobs every minute (every scheduler: pg_cron, the worker, Vercel / external cron): jobs whose start was lost, retries that are due, the next chunk of long jobs, and jobs whose invocation died (their lease expired). /api/cron/user-jobs does the same on demand (processor secret).
  • Each invocation spends at most USER_JOB_BUDGET_MS (default 50 s) on a job; long jobs hand over in chunks and continue in a fresh invocation, so serverless time limits are respected.
  • Failures from unexpected errors are retried with backoff (15 s, 1 min, 4 min…) up to the job kind's limit; permission and validation errors fail at once.
  • Results are stored in the private job-results bucket of the configured storage provider and served only through the authorised download routes. user-jobs-cleanup (every 15 minutes) deletes expired jobs and their files; USER_JOB_RESULT_TTL_HOURS (default 168) sets how long they are kept.
  • Live updates use the realtime provider (Supabase Realtime or SSE) with polling as the fallback.