Deployment

Deploy Automations

Run scheduled and event-driven jobs durably on Cloudflare, Vercel, and Azure.

Declare tasks once and let the selected cloud continue them after the accepting request ends. Automations support named checkpoints, cron schedules, durable delays, event waits, cancellation, and retries for explicitly idempotent actions. The automatic deployment path currently requires Nuxt.

1. Declare a Host and Schedule

Add this declaration to your existing cloud.config.ts:

cloud.config.ts
deploy: {
  target: 'vercel', // 'cloudflare' | 'vercel' | 'azure'
  automations: {
    entry: 'server/cloudconfig/automations.ts',
    schedules: {
      daily: {
        cron: '0 9 * * *',
        timeZone: 'Europe/Amsterdam',
        run: {
          task: 'report',
          scope: { ownerId: null, workspace: 'operations' },
          payload: { report: 'daily' },
        },
      },
    },
  },
  secrets: ['REPORT_ENDPOINT', 'REPORT_TOKEN'],
},

The host default-exports defineAutomationHost. Its authorization method runs at acceptance, on status and control operations, and before every continuation. This example permits only a trusted operations task:

server/cloudconfig/automations.ts
import { defineAutomationHost } from '@cloudconfig/core/automations'

export default defineAutomationHost({
  async authorize(request) {
    if (
      request.task !== 'report' ||
      request.scope.ownerId !== null ||
      request.scope.workspace !== 'operations'
    ) throw new Error('This job belongs to operations.')
  },
  tasks: {
    async report(payload, context) {
      return context.step('send-report', async ({ idempotencyKey, signal }) => {
        const endpoint = context.env?.REPORT_ENDPOINT
        const token = context.env?.REPORT_TOKEN
        if (typeof endpoint !== 'string' || typeof token !== 'string') {
          throw new Error('Configure REPORT_ENDPOINT and REPORT_TOKEN.')
        }
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: {
            authorization: `Bearer ${token}`,
            'content-type': 'application/json',
            'idempotency-key': idempotencyKey,
          },
          body: JSON.stringify(payload),
          signal,
        })
        if (!response.ok) throw new Error(`Report service returned ${response.status}.`)
        return { sent: true }
      }, { idempotent: true, attempts: 3 })
    },
  },
})

Enable retries only when the receiving service actually deduplicates the action or repeating it is safe. Sending an idempotency header alone does not establish that guarantee. Store credentials in server environment variables; job payloads and checkpoint results must contain JSON values and may enter native execution history. Keep task modules portable: Azure runs them outside Nuxt, so use explicit imports and the supplied context.env instead of Nuxt aliases or request state.

2. Install and Deploy

Install the same native runtime dependencies used by agents, then run:

pnpm exec cloudconfig doctor
pnpm exec cloudconfig deploy --dry-run
pnpm exec cloudconfig deploy

Deployment creates a private CLOUDCONFIG_AUTOMATION_ADMIN_KEY in the ignored .env, uploads it as a server secret, publishes the app, and synchronizes config-declared schedules through POST /_cloudconfig/automations/configure. This authenticated endpoint accepts only schedules from the deployed source; request bodies cannot submit arbitrary jobs. Schedules start without an app visit. A protected Vercel deployment also needs VERCEL_AUTOMATION_BYPASS_SECRET locally or in CI for this request.

An unchanged schedule retains its next occurrence across deployments. Changing one replaces its native timer; removing one cancels future occurrences. Runtime schedules created with platform.schedule(key, value) have a separate namespace. Cron has five fields and uses the declared IANA time zone. A delayed scheduler enqueues the recorded occurrence, then advances to the next future occurrence; it does not backfill every missed interval.

If schedule synchronization fails, the CLI reports that the app was published but schedules were not configured. Correct startup or deployment-protection settings and retry deployment. A dry run never generates this key or calls this endpoint.

Where Execution Lives

  • Cloudflare: a separate SQLite Durable Object class stores jobs, receipts, leases, and schedules. Alarms continue work. Defaults are CloudconfigAutomations, CLOUDCONFIG_AUTOMATIONS, and migration tag cloudconfig-automations-v1; override them under deploy.automations.cloudflare when adopting existing resources. Keep identities stable across deployments.
  • Vercel: the official Workflow compiler builds job and schedule functions with queue triggers. Private records use the configured application database, normally Turso, in the historical _cloudconfig_agent_records table. Agents and automations use different record keys. Native sleeps suspend delays and event waits; event waits also recheck every 60 seconds.
  • Azure: agents and automations share one separate Durable Functions app and private Storage account. Blob records hold checkpoints and results; native timers and external events continue jobs. The web app receives a private CLOUDCONFIG_AZURE_AUTOMATION_ENDPOINT and Function key automatically. Existing deploy.azure.agents.appName and storageAccount options name this shared execution app. They do not select the application's database or upload storage.

Schedules use a stable run ID for each occurrence to deduplicate native retries. Cloudflare routes each scheduled job to its owner's Durable Object. Vercel starts a new scheduling execution after each occurrence; Azure uses continueAsNew to bound scheduling history.

Start, Signal, and Cancel a Job

In an authenticated Nuxt server route, derive the owner and workspace from the session, verify access, and call the server utility:

const platform = useCloudConfigAutomations(event)
const runs = await platform.runs(scope)
await runs.start({ scope, runId, task: 'process-order', payload: { orderId } })
const result = await runs.status(runId)
await runs.signal(runId, 'approved', { approved: true })
await runs.cancel(runId)

Implement the corresponding task and authorization in your host. Never trust a browser-supplied owner, workspace, or system scope. Outside a request, use getAutomationPlatform(env) with the activity's environment. Both native ports are bound inside workers, so jobs can start agents and agent callbacks can schedule jobs.

Inside a task, context.sleep('delay', milliseconds) persists a deadline and context.waitForEvent('approved') pauses until that named event arrives. Signals are buffered; the first payload for each event name wins, including signals sent before the wait. runs.resume(runId) nudges a nonterminal execution after an infrastructure interruption. Failed business tasks remain failed; review their results before starting a new run.

Use stable, unique checkpoint names and keep external I/O inside context.step. Each native activity performs at most one new action and replays completed receipts. Honor the action's deadline signal and keep individual actions under three minutes; split longer work into checkpoints. There are at most 1,000 named checkpoints per job. An interrupted non-idempotent action fails for review because its external outcome is uncertain. Cancellation prevents future work; an external service may already have completed an in-flight action.

Integration Jobs

With Connect enabled and deploy.automations declared, CloudConfig adds token refresh, encryption-key rotation, registered MCP client-secret rotation, and abandoned-connection cleanup every five minutes. The bounded sweep saves its cursor between runs. Tune connect.maintenance: { cron, timeZone, limit } for larger installations; limit defaults to 50 and may be 1–500. Set connect.maintenance: false when another scheduler owns housekeeping. Keep older encryption keys until the entire sweep succeeds; inspect maintenance results for failed rows before removing them.

Set connect.webhooks: { task: 'provider-event' } to dispatch verified integration events to a task in your host. The payload is a WebhookDelivery with a public connection summary, not account credentials. The run ID includes provider, event, connection, and delivery identity. Vendor retries reuse that ID. Connect acknowledges the event only after durable acceptance; unavailable queues return an error. Existing onConnectWebhook handlers still run inline, so keep them short and do long work in tasks. Recheck connection ownership and permissions when a task later uses account credentials.

The archived dashboard automation editor is not restored by this runtime. Cloudflare-specific deploy.workflows classes also remain Cloudflare-specific; use the task API above for portable jobs. Changing hosts does not migrate in-flight native jobs. Drain the previous runtime and recreate schedules on the destination.

Copyright © 2026