Skip to Content
⚠️Active Development Notice: TimeTiles is under active development. Information may be placeholder content or not up-to-date.

Background Jobs

TimeTiles uses Payload CMS’s built-in job queue and workflow system  for all asynchronous processing. Jobs and workflows are defined in lib/jobs/ and registered through ALL_JOBS in lib/jobs/ingest-jobs.ts and the workflow definitions in lib/jobs/workflows/. See the Payload docs for tasks , workflows , and queues .

Key Behavior

  • Auto-deletion: Completed jobs are automatically deleted (deleteJobOnComplete: true)
  • Workflow-based orchestration: The ingestion pipeline uses 4 Payload Workflows, queued by collection hooks and trigger services
  • 3-queue architecture: ingest (user-facing workflows), default (trigger jobs), maintenance (scheduled system jobs)
  • Production: One Docker worker container per queue runs Payload’s jobs:run command; see the worker settings below
  • Development: Payload’s autoRun schedules and processes all queues within the Next.js process after Payload initializes; no polling process or seeded admin login is required

Native job execution, scheduling, queueing and cancellation are admin-only through jobs.access. The payload-jobs collection’s CRUD permissions are separate and do not guard native job endpoints. Trusted server-side workers use the Local API’s default access override. The generated payload-jobs-stats global is admin-readable and not manually writable, including by admins. It holds scheduling state; Payload’s scheduler maintains it through its database adapter.

Production Workers

deployment/docker-compose.prod.yml is the source of truth for worker commands. Each worker consumes exactly one queue:

QueuePolling cron (--cron)Batch limit (--limit)--handle-schedules
ingest*/10 * * * * * (every 10 seconds)10No
default*/1 * * * * (every minute)20Yes
maintenance*/1 * * * * (every minute)20Yes

--cron needs a quoted cron expression. Polling consumes queued work; --handle-schedules also queues due tasks from their native Payload schedule definitions. Scheduling is filtered by --queue, so both default and maintenance need the flag. Enabling it only on default does not schedule maintenance tasks. The ingest queue receives workflows from application triggers.

For example, the equivalent default-queue worker command from the repository root is:

pnpm --filter web payload jobs:run --cron '*/1 * * * *' --queue default --limit 20 --handle-schedules

Ingest Workflows

The ingestion pipeline is orchestrated by 4 Payload Workflows. Each workflow sequences multiple task handlers into a linear pipeline. See Data Ingestion Pipeline for detailed stage documentation.

WorkflowTriggerPipeline
manual-ingestingest-files afterChange hookdataset-detection, then per-sheet: analyze, detect-schema, validate, create-schema-version, geocode, create-events
scheduled-ingestShared trigger service: scheduler, webhook, manual run, wizard, or data-package activationurl-fetch, dataset-detection, then per-sheet pipeline
scraper-ingestScheduler, webhook, or manual runscraper-execution, dataset-detection, then per-sheet pipeline
ingest-processReview approval hook or retry/reset routesResume from the selected stage

All ingest workflows run on the ingest queue with per-resource concurrency keys (e.g., ingest:manual:{id}, ingest:scheduled:{id}).

Error Model

  • Throw for transient failures — Payload retries the task
  • Return { needsReview: true } for human review — pipeline pauses for that sheet
  • Return data for success — pipeline continues to next task
  • Multi-sheet files use Promise.allSettled with per-sheet try/catch; individual sheet failures do not block other sheets

For scheduled-ingest, Payload owns the retry decision. The workflow propagates failures without incrementing the schedule’s failure counter. On its next pass, schedule-manager reconciles the current run’s latest Payload job when hasError is final, then plans new runs. This normally happens on the next minute tick while scheduled execution is enabled and workers are processing the queue. The reconciliation locks and re-reads the schedule, ignores jobs older than its current lastRun, and records the failed run only once. Jobs marked error.cancelled are excluded: an explicit cancellation must not consume the schedule’s retry budget. Do not duplicate Payload’s retry calculation in workflow catches.

Stuck-run cleanup cancels old queued jobs through payload.jobs.cancel(), with a processing: false filter applied at cancellation time. Payload preserves completed and already failed jobs; cancelled jobs have error.cancelled: true and no completion timestamp. They are removed by the existing seven-day failed-job retention, not the three-day completed-job retention.

Task Definitions and Scheduling

The current task inventory is ALL_JOBS in lib/jobs/ingest-jobs.ts. Each handler owns its task slug, retry policy, and any native Payload schedule configuration. Inspect those definitions for exact cron expressions and queue assignments instead of copying a schedule table.

Workflow task slugs can differ from filenames: schema-detection-job.ts registers detect-schema, and create-events-batch-job.ts registers create-events. Use the registered slug when invoking a task.

Post-review processing is not always a schema-version restart. ingest-process supports analyze-duplicates, detect-schema, create-schema-version (default), and create-events as resume points.

Adding a New Job

  1. Create handler in lib/jobs/handlers/my-job.ts
  2. Import and register the job config in ALL_JOBS in lib/jobs/ingest-jobs.ts
  3. Configure its queue, retry policy, and schedule if applicable
  4. If it is a workflow task, add it to the appropriate workflow in lib/jobs/workflows/
  5. Generate a migration with make migrate-create if the job needs schema changes

Testing Jobs

See Integration Testing Patterns for job testing. Key points:

  • Query pending jobs before running (completedAt: { exists: false })
  • Verify side effects after successful runs (completed job records are deleted). Final failures remain in payload-jobs and can be inspected.
  • Use describe.sequential() for tests that interact with the job queue
  • Use a bounded drain loop with payload.jobs.run({ allQueues: true }) to process chained workflow tasks in tests
Last updated on