How to Use the Automations Runtime in OpenWork: Complete Implementation Guide

The Automations Runtime in OpenWork provides exact-once execution semantics through a MySQL-backed lease system that persists schedules, manages worker claims, and recovers from crashes using deterministic occurrence IDs.

The OpenWork automation platform separates scheduling logic from execution infrastructure using two distinct layers. This guide demonstrates how to integrate with the Automations Runtime (the Den execution engine) to build reliable background workers that claim, execute, and report automation tasks without duplicate side-effects. We reference the official different-ai/openwork source implementation to ensure technical accuracy.

Understanding the Two-Layer Architecture

OpenWork’s automation system is intentionally split into pure contracts and runtime infrastructure:

  • Automation Domain: A TypeScript-only package (@openwork/automations) that defines schedules, occurrence IDs, and claim receipts without side effects. This layer lives in dev/packages/automations/ and provides deterministic scheduling logic.

  • Automation Runtime (Den): The persistence and execution layer implemented in dev/ee/apps/den-api/src/automations/. It provides MySQL storage, REST API endpoints for claiming work, and recovery mechanisms for missed runs.

The runtime acts as a thin engine adapter that implements the domain interfaces. When your code schedules an automation, the runtime stores it in MySQL and calculates the next occurrence ID, enabling any client (desktop app, CI pipeline, or microservice) to claim the work via HTTP.

Setting Up the Automation Domain

Add the domain package to any TypeScript project that defines or executes automations:

pnpm add @openwork/automations

Import the type definitions that describe runtime interactions:

import type {
  AutomationClaimResult,
  AutomationListItem,
} from '@openwork/automations';

These types enable compile-time safety when communicating with the Den runtime endpoints. The domain code guarantees that identical schedules generate the same deterministic occurrence ID, which is essential for idempotent retries.

Registering Automations with the Runtime

Use the scheduleAutomation helper to persist a schedule to the Den database. The function POSTs to the /automations endpoint defined in dev/ee/apps/den-api/src/automations/service.ts:

import { scheduleAutomation } from '@openwork/automations';

// Schedule a daily report for 08:00 UTC
await scheduleAutomation({
  name: 'daily-report',
  cron: '0 8 * * *',
  // Optional: start/end dates, timezone overrides
});

The runtime stores the schedule in MySQL and pre-calculates occurrence IDs. According to the source in dev/ee/apps/den-api/src/automations/repository.ts, the persistence layer maintains these records with timezone-aware next-run calculations.

Claiming and Executing Runtime Work

A runner queries the Den runtime for due work, claims exclusive execution rights using a leased receipt, and reports the outcome. This three-step protocol prevents duplicate execution across distributed clients.

Fetching Due Automations

Query the runtime for automations ready to execute:

const due: AutomationListItem[] = await fetch(
  'https://api.openwork.local/automations/due',
  { headers: { Authorization: `Bearer ${TOKEN}` } }
).then(r => r.json());

The /automations/due endpoint is implemented in dev/ee/apps/den-api/src/automations/service.ts and returns only items whose next occurrence timestamp has passed and are not currently leased by another healthy runner.

Claiming Work Items

Claim an automation to receive a stable receipt that survives process restarts:

import { claimAutomation } from '@openwork/automations';

const claim: AutomationClaimResult = await claimAutomation({
  automationId: due[0].id,
  runnerId: 'my-desktop-client',
});

The claimAutomation function generates a unique receipt stored in MySQL. This receipt acts as a lease—other runners cannot claim the same occurrence until the recovery window expires.

Reporting Execution Results

After executing your business logic, report the outcome to release the lease:

import { reportAutomationResult } from '@openwork/automations';

// Success case
await reportAutomationResult({
  receipt: claim.receipt,
  status: 'succeeded',
  result: { processedCount: 42 },
});

// Failure case
await reportAutomationResult({
  receipt: claim.receipt,
  status: 'failed',
  result: { error: 'Network timeout' },
});

The runtime updates the occurrence record in repository.ts and schedules the next invocation based on the cron expression.

Handling Recovery and Exact-Once Execution

The Automations Runtime guarantees exact-once execution through a configurable recovery mechanism. If a runner crashes after claiming but before reporting, the receipt persists in MySQL with a timestamp.

The recovery logic in dev/ee/apps/den-api/src/automations/service.ts uses AUTOMATION_MIN_CLAIM_WINDOW_MS (default approximately 15 minutes) to determine when a claim has expired. After this window, another runner can safely claim the same occurrence ID and receive a new receipt referencing the original occurrence.

This design prevents duplicate side-effects: the original receipt becomes invalid after the window expires, and the new claim inherits the same occurrence ID without generating a new scheduled instance.

Complete End-to-End Implementation

Below is a production-ready worker that schedules an automation and polls for execution:

import {
  scheduleAutomation,
  claimAutomation,
  reportAutomationResult,
  type AutomationListItem,
} from '@openwork/automations';

// 1. Register schedule (run once during setup)
await scheduleAutomation({
  name: 'sync-library',
  cron: '0 * * * *', // Every hour at minute 0
});

// 2. Background worker loop
async function automationWorker() {
  const resp = await fetch(
    'https://api.openwork.local/automations/due',
    { headers: { Authorization: `Bearer ${process.env.OPENWORK_TOKEN}` } }
  );
  const due = (await resp.json()) as AutomationListItem[];

  for (const item of due) {
    const claim = await claimAutomation({
      automationId: item.id,
      runnerId: 'desktop-client-v2.1',
    });

    try {
      await syncLibrary(); // Your business logic here
      await reportAutomationResult({
        receipt: claim.receipt,
        status: 'succeeded',
        result: { syncedAt: new Date().toISOString() },
      });
    } catch (error) {
      await reportAutomationResult({
        receipt: claim.receipt,
        status: 'failed',
        result: { error: (error as Error).message },
      });
    }
  }
}

setInterval(automationWorker, 30_000); // Poll every 30 seconds

This implementation relies entirely on the Den runtime for persistence and lease management while keeping the client code stateless.

Summary

  • Layered Architecture: The Automations Runtime separates pure scheduling logic (@openwork/automations) from MySQL-backed execution infrastructure (den-api).
  • Lease-Based Claims: Workers claim work using stable receipts that enable exact-once execution across crashes and restarts.
  • Recovery Windows: Missed runs are automatically recoverable for approximately 15 minutes via AUTOMATION_MIN_CLAIM_WINDOW_MS before another worker can safely take over.
  • REST Interface: All runtime operations occur through HTTP endpoints defined in dev/ee/apps/den-api/src/automations/service.ts.
  • Idempotency Guarantee: Deterministic occurrence IDs ensure that retried claims never generate duplicate scheduled instances.

Frequently Asked Questions

What happens if my runner crashes after claiming an automation?

The receipt remains valid in the MySQL database for the duration of AUTOMATION_MIN_CLAIM_WINDOW_MS (default ~15 minutes). If your runner crashes, the automation appears in the /automations/due endpoint again after the window expires, allowing another runner to claim it with a fresh receipt referencing the same occurrence ID. This prevents lost work without risking duplicate execution.

How does the runtime prevent duplicate automation runs?

The runtime implements exact-once semantics through deterministic occurrence IDs and leased receipts. When a schedule triggers, the runtime generates a unique occurrence ID stored in dev/ee/apps/den-api/src/automations/repository.ts. Claiming creates a leased receipt tied to that ID; until the lease expires or the result is reported, no other runner can process that specific occurrence.

Can I use the Automations Runtime without the OpenWork desktop application?

Yes. The runtime exposes a language-agnostic REST API. Any HTTP client—including CI pipelines, serverless functions, or custom microservices—can poll /automations/due, claim work via the claim endpoint, and report results back to dev/ee/apps/den-api/src/automations/service.ts. You only need the @openwork/automations package for TypeScript type definitions.

Where are automation schedules physically stored?

Schedules, occurrence history, and claim receipts persist in MySQL through the repository layer defined in dev/ee/apps/den-api/src/automations/repository.ts. The database maintains tables for automation definitions, calculated next-run timestamps, and execution receipts with recovery window timestamps.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →