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.
Use Schedules when you want platform-managed recurring jobs with delivery tracking, retries, and destinations managed outside your app code.
Use the @agentuity/schedule standalone package to access this service from any Node.js or Bun app without the runtime.
There are two ways to run scheduled work:
- Cron routes 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 | In-process scheduled handlers with access to route services on c.var.* |
| 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 |
| CLI | agentuity cloud schedule | Manage schedules from the command line |
| Web App | Web App | 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.
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
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
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
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.
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:
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:
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
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 };
},
});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:
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
listDeliveriesor 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: In-process scheduled handlers that run inside your deployed app
- Queues: Async, event-driven processing for work that doesn't need a fixed time trigger
- Sandbox: Isolated execution environments used as schedule destinations