Skip to content

Scheduled jobs

APCO can run durable, project-scoped commands from the same immutable artifact as an SSR or OCI-image deployment. Definitions live in apco.yml; the dashboard, CLI, HTTP API, and MCP control scheduling and inspect logical runs without exposing commands or environment values.

Configure a job

yaml
app:
  name: reports
  type: ssr

database:
  enabled: true

jobs:
  - name: nightly-report
    schedule: "0 2 * * *"
    timezone: Europe/Berlin
    command: node scripts/report.mjs
    channels: [production]
    timeoutSeconds: 300
    retries: 2
    retryDelaySeconds: 30
    concurrency: forbid
    enabled: true

Schedules are standard five-field cron expressions. APCO validates the expression and IANA timezone at manifest load, normalizes whitespace, and computes daylight-saving transitions in the selected timezone. Job names are stable lowercase identifiers, unique within one manifest. Scheduled jobs require app.type: ssr or dockerimage because static deployments have no runnable application image. An OCI image must provide /bin/sh, and its jobs may target Production/Preview but not Dev.

The schema accepts at most 50 definitions, while the owner's live plan controls how many project definitions and concurrent logical runs may execute. A deployment above its plan allowance is rejected; existing definitions are retained but paused if the plan later loses access. APCO never deletes resources automatically after a downgrade.

Channel and artifact isolation

Each definition declares supported channels (production, preview, and—for source SSR only—dev). Activation snapshots the command and execution settings against the exact running deployment and immutable image:

  • Production follows the current running Production deployment.
  • Dev follows projects.active_dev_deployment_id and pauses when Dev stops.
  • A CLI/GitHub Actions Preview follows the project's active legacy Preview pointer.
  • Each GitHub App PR Preview is an independent scope. Pass its exact deployment id when listing or controlling it.

The dispatcher proves that exact route pointer again before creating a scheduled run. Promotion, rollback, deployment activation, and scheduler reconciliation use the same project advisory lock, so a run cannot be created against a scope while its active artifact is changing. A logical run then remains pinned to that image even if a newer deployment activates.

The one-shot container uses gVisor, the owner's CPU/memory/process limits, the project's network, the exact channel environment, and only that channel's managed DATABASE_URL. It never joins ingress and cannot receive web traffic. APCO overrides APCO_JOB_NAME, APCO_JOB_RUN_ID, APCO_JOB_ATTEMPT, APCO_JOB_TRIGGER, APCO_JOB_SCHEDULED_AT, APCO_DEPLOYMENT_ID, and APCO_DEPLOYMENT_CHANNEL with trusted platform values.

Control and inspect jobs

bash
apco jobs ls --project my-app --channel production --json
apco jobs run nightly-report --project my-app --channel production --json
apco jobs disable nightly-report --project my-app --channel production --json
apco jobs enable nightly-report --project my-app --channel production --json
apco jobs runs nightly-report --project my-app --channel production --json
apco jobs logs <run-id> --project my-app --json
apco jobs cancel <run-id> --project my-app --json

For a GitHub App PR Preview, add --channel preview --deployment <exact-deployment-id>. A bare Preview filter deliberately addresses only the stable legacy Preview scope. jobs logs --follow is human streaming mode and cannot be combined with --json; agents should poll the JSON command.

Disabling a job is a durable user override retained across later deployments with the same scope and job name. enabled: false is the manifest default state; the dashboard/API override cannot make a manifest-disabled definition runnable. run queues a manual logical run for an active scope even when automatic scheduling is disabled, which makes acceptance testing immediate.

Concurrency, retries, and cancellation

concurrency is evaluated against non-terminal runs of the same definition:

  • forbid skips the newer run with overlap_forbidden.
  • allow lets both run, subject to the project plan's concurrent-run limit.
  • replace cancels older queued/waiting runs and requests termination of an older running attempt before retrying the newer run.

Timeout and cancellation send SIGTERM, wait ten seconds, then use SIGKILL if needed. Retries create explicit attempt rows under one stable logical run id and use capped exponential delay. Queue delivery is recoverable: committed run rows are the durable outbox, and worker startup/minute reconciliation resends missing queue work. Scheduled occurrences have a unique dispatch key; after downtime APCO creates at most one catch-up occurrence and advances to a future schedule.

Commands should still be idempotent. Use the stable APCO_JOB_RUN_ID as an application-level idempotency key for side effects, and commit work transactionally when possible. APCO prevents duplicate platform attempts; it cannot make an external API side effect atomic with your database.

Logs, retention, and alerts

Stdout and stderr are combined, secret-redacted, and capped at 1 MiB per attempt. Safe APIs expose schedule metadata, logical state, bounded errors, attempt status, and redacted logs; they never serialize the command, pinned image, environment values, or container name. jobs:read reads definitions/history, jobs:write controls them, and logs additionally require logs:read.

Final failure or timeout opens a scheduled_job_failure incident for the definition and uses the project's normal alert delivery policy. A later successful run resolves it. Terminal history/log retention follows platform housekeeping; active/non-terminal run artifacts are protected from image pruning.

Lifecycle behavior

  • Project suspension pauses all definitions and cancels queued/waiting work while requesting cancellation of running work.
  • A plain Production stop pauses Production definitions; Dev remains independent unless it is explicitly stopped or the whole project is suspended.
  • Replacing a deployment retires removed names, updates changed definitions, and preserves user enable/disable overrides.
  • Promotion creates fresh Production definitions from the promoted artifact; Preview definitions remain on their Preview scope.
  • Rollback reconciles definitions to the newly active rollback artifact without replaying old runs.

See the manifest fields, CLI reference, and HTTP routes.

APCO Cloud — ship apps with one command.