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

> Implement OpenWork Automations Runtime for exact-once execution. Learn how its MySQL-backed lease system manages schedules and worker claims for reliable crash recovery.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-21

---

**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:

```bash
pnpm add @openwork/automations

```

Import the type definitions that describe runtime interactions:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/dev/ee/apps/den-api/src/automations/service.ts):

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.