# Schedules

Create platform-managed cron jobs with HTTP and sandbox destinations

When a job needs to run on a timer, like data syncs, nightly cleanup, or report generation, Schedules let you configure timing and destinations outside your app code. The platform calls your endpoints on a cron schedule, tracks each delivery, and retries on failure.

> [!WARNING]
> **Schedules replace Cron for new work**
> Use Schedules when you want platform-managed recurring jobs with delivery tracking, retries, and destinations managed outside your app code.

> [!TIP]
> **Not inside an agent or route?**
> Use the [`@agentuity/schedule`](/reference/standalone-packages#schedules) standalone package to access this service from any Node.js or Bun app without the runtime.

> [!NOTE]
> **Schedules vs Cron Routes**
> There are two ways to run scheduled work:
>
> - [Cron routes](/routes/cron) are route handlers in your app, triggered on a schedule via HTTP POST. The schedule is defined in code and deployed with your project.
> - **Schedules** are platform-managed resources. The platform calls your destinations (HTTP URLs or sandboxes) on schedule, with per-delivery tracking and automatic retries.
>
> Use cron routes when you want in-process scheduling with route services on `c.var.*`. Use Schedules when you need delivery tracking, multiple destination types, or want to manage job timing independently of deployments.

## When to Use Schedules

| Approach | Best For |
|----------|----------|
| **Schedules** | Platform-managed recurring jobs with delivery tracking and multiple destination types |
| [Cron Routes](/routes/cron) | In-process scheduled handlers with access to route services on `c.var.*` |
| [Queues](/services/queues) | Event-driven async processing, not time-based |

## Managing Schedules

| Context | Access | Details |
|---------|--------|---------|
| Agents | `ctx.schedule` | See examples below |
| Routes | `c.var.schedule` | See [Using in Routes](#using-in-routes) |
| CLI | `agentuity cloud schedule` | Manage schedules from the command line |
| Web App | [Web App](https://app.agentuity.com/services/schedule) | Create and inspect schedules in the UI |

## Creating Schedules

Pass a `name`, an `expression` (standard five-field cron), and an optional array of `destinations`. The schedule and its destinations are created atomically: if any destination fails validation, the entire operation is rolled back.

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('ScheduleSetup', {
  handler: async (ctx, input) => {
    const result = await ctx.schedule.create({
      name: 'Daily Report',
      description: 'Generate and email daily reports',   // optional
      expression: '0 9 * * 1-5',  // weekdays at 9am
      destinations: [
        {
          type: 'url',
          config: {
            url: 'https://example.com/reports/generate',
            method: 'POST',
            headers: { 'Authorization': 'Bearer my-token' },
          },
        },
      ],
    });

    ctx.logger.info('Schedule created', {
      id: result.schedule.id,
      nextRun: result.schedule.due_date,
    });

    return { scheduleId: result.schedule.id };
  },
});
```

### Common Cron Expressions

| Expression | Fires |
|------------|-------|
| `* * * * *` | Every minute |
| `*/5 * * * *` | Every 5 minutes |
| `0 * * * *` | Every hour |
| `0 0 * * *` | Daily at midnight |
| `0 9 * * 1-5` | Weekdays at 9am |
| `0 0 * * 1` | Weekly on Monday at midnight |
| `0 0 1 * *` | Monthly on the 1st at midnight |

The server validates cron expressions on creation and update. `due_date` on the schedule reflects the next computed execution time.

## Destination Types

Each schedule can have multiple destinations. When the schedule fires, the platform sends a request to each destination.

| Type | Config | Description |
|------|--------|-------------|
| `url` | `{ url, headers?, method? }` | Sends an HTTP request to the URL on schedule |
| `sandbox` | `{ sandbox_id, command? }` | Runs a command in a sandbox on schedule |

### URL Destination

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('WebhookScheduler', {
  handler: async (ctx, input) => {
    const { destination } = await ctx.schedule.createDestination(input.scheduleId, {
      type: 'url',
      config: {
        url: 'https://example.com/webhook/sync',
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${input.apiToken}`,
          'Content-Type': 'application/json',
        },
      },
    });

    ctx.logger.info('URL destination added', { destinationId: destination.id });
    return { destinationId: destination.id };
  },
});
```

### Sandbox Destination

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('SandboxScheduler', {
  handler: async (ctx, input) => {
    const { destination } = await ctx.schedule.createDestination(input.scheduleId, {
      type: 'sandbox',
      config: {
        sandbox_id: input.sandboxId,
        command: 'bun run src/run/sync.ts',
      },
    });

    ctx.logger.info('Sandbox destination added', { destinationId: destination.id });
    return { destinationId: destination.id };
  },
});
```

## Listing and Updating Schedules

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('ScheduleManager', {
  handler: async (ctx, input) => {
    // List schedules with optional pagination
    const { schedules, total } = await ctx.schedule.list({ limit: 20, offset: 0 });
    ctx.logger.info('Schedules', { count: schedules.length, total });

    // Get a specific schedule with its destinations
    const { schedule, destinations } = await ctx.schedule.get(input.scheduleId);
    ctx.logger.info('Next run', { dueDate: schedule.due_date });

    // Update the cron expression: due_date is automatically recomputed
    const { schedule: updated } = await ctx.schedule.update(input.scheduleId, {
      expression: '0 0 * * *',  // change to daily at midnight
    });

    return { nextRun: updated.due_date, destinationCount: destinations.length };
  },
});
```

## Delivery Tracking

Each time a schedule fires, one delivery record is created per destination. Use `listDeliveries` to inspect delivery history and check for failures.

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('DeliveryInspector', {
  handler: async (ctx, input) => {
    const { deliveries } = await ctx.schedule.listDeliveries(input.scheduleId);

    for (const delivery of deliveries) {
      ctx.logger.info('Delivery record', {
        date: delivery.date,
        status: delivery.status,      // 'pending' | 'success' | 'failed'
        retries: delivery.retries,    // number of retry attempts
        error: delivery.error,        // null on success
      });
    }

    const failed = deliveries.filter(d => d.status === 'failed');
    return { total: deliveries.length, failed: failed.length };
  },
});
```

### Delivery Statuses

| Status | Description |
|--------|-------------|
| `pending` | Delivery is queued and has not been attempted yet |
| `success` | The destination received the request and responded successfully |
| `failed` | The delivery encountered an error; `error` field contains the reason |

The `retries` field on each delivery record tracks how many retry attempts were made before the final status was recorded.

## Fetching a Specific Destination or Delivery

Two convenience methods let you look up a single record by ID without iterating manually.

`getDestination(scheduleId, destinationId)` fetches the schedule via `get()` and returns the matching destination:

```typescript
const dest = await ctx.schedule.getDestination(input.scheduleId, input.destinationId);
ctx.logger.info('Destination type', { type: dest.type });
```

`getDelivery(scheduleId, deliveryId, params?)` calls `listDeliveries()` and returns the matching record. Pass `params` to control the pagination window if the delivery may not be in the first page:

```typescript
const delivery = await ctx.schedule.getDelivery(input.scheduleId, input.deliveryId);
ctx.logger.info('Delivery status', { status: delivery.status, retries: delivery.retries });
```

Both methods throw if the ID is not found in the result set.

## Removing Destinations and Schedules

```typescript
import { createAgent } from '@agentuity/runtime';

const agent = createAgent('ScheduleCleaner', {
  handler: async (ctx, input) => {
    // Remove a single destination (keeps the schedule)
    await ctx.schedule.deleteDestination(input.destinationId);

    // Delete a schedule and all its destinations and delivery history
    await ctx.schedule.delete(input.scheduleId);

    return { deleted: true };
  },
});
```

> [!WARNING]
> **Destructive Operation**
> `delete()` permanently removes the schedule, all its destinations, and all delivery history. This operation cannot be undone.

## Using in Routes

Routes access the schedule service via `c.var.schedule`:

```typescript
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';

const router = new Hono<Env>();

router.post('/schedules', async (c) => {
  const body = await c.req.json();

  const result = await c.var.schedule.create({
    name: body.name,
    expression: body.expression,
    destinations: body.destinations,
  });

  return c.json({ scheduleId: result.schedule.id, nextRun: result.schedule.due_date });
});

router.get('/schedules/:id/deliveries', async (c) => {
  const scheduleId = c.req.param('id');
  const { deliveries } = await c.var.schedule.listDeliveries(scheduleId);

  return c.json({ deliveries });
});

export default router;
```

## Best Practices

- **Use descriptive names**: names like `"Daily User Report - EU"` are easier to manage than `"job-1"` when listing schedules in the dashboard
- **Keep destinations idempotent**: the platform may retry failed deliveries, so ensure your endpoints handle duplicate invocations safely
- **Monitor delivery history**: poll `listDeliveries` or check the dashboard to catch recurring failures before they accumulate
- **Remove unused destinations before changing schedules**: deleting a destination does not affect other destinations on the same schedule, making partial updates safe

## Next Steps

- [Cron Routes](/routes/cron): In-process scheduled handlers that run inside your deployed app
- [Queues](/services/queues): Async, event-driven processing for work that doesn't need a fixed time trigger
- [Sandbox](/services/sandbox): Isolated execution environments used as schedule destinations