Schedules API

Schedules automate recurring task execution. Each schedule defines a prompt, an interval (cron, shorthand, or natural language), and an optional agent assignment. Stagent supports two schedule types: standard scheduled tasks that fire on a cron cadence, and heartbeat monitors that evaluate a checklist and only act when conditions warrant. The Schedules API supports CRUD operations, interval parsing preview, and heartbeat history retrieval.

Quick Start

Preview how an interval parses, create a recurring schedule, and check its firing history:

// 1. Parse the interval to preview how it resolves before committing
const parsed: ParseResult = await fetch('/api/schedules/parse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expression: 'every weekday at 9am' }),
}).then(r => r.json());

console.log(parsed.cronExpression);  // "0 9 * * 1-5"
console.log(parsed.description);     // "At 09:00 AM, Monday through Friday"
console.log(parsed.nextFireTimes);   // ["2026-04-06T09:00:00Z", ...]
console.log(parsed.confidence);      // 0.95

// 2. Create the schedule — it starts firing immediately
const schedule: Schedule = await fetch('/api/schedules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  name: 'Daily standup summary',
  prompt: 'Review all open tasks, summarize blockers, and draft a standup message',
  interval: 'every weekday at 9am',
  projectId: 'proj-8f3a-4b2c',
  assignedAgent: 'claude-code',
  agentProfile: 'project-manager',
}),
}).then(r => r.json());
// → { id: "sched-e7b2-1f4a", status: "active", nextFireAt: "2026-04-06T09:00:00Z", ... }

// 3. Check the schedule detail to see past firings
const detail: ScheduleDetail = await fetch(`/api/schedules/${schedule.id}`)
.then(r => r.json());

console.log(`Fired ${detail.firingCount} times`);
detail.firingHistory.forEach(task => {
console.log(`  [${task.status}] ${task.title} — ${task.createdAt}`);
});

Base URL

/api/schedules

Endpoints

List Schedules

GET /api/schedules

Retrieve all schedules ordered by newest first.

Response 200 — Array of schedule objects

Schedule Object

FieldTypeReqDescription
idstring (UUID)*Schedule identifier
namestring*Schedule display name
promptstring*Task prompt executed on each firing
cronExpressionstring*Parsed cron expression
projectIdstring (UUID)Associated project
assignedAgentstringAgent runtime ID
agentProfilestringProfile ID for execution
typeenum*scheduled or heartbeat
statusenum*active, paused, or exhausted
recursboolean*Whether the schedule repeats
maxFiringsnumberMaximum number of firings before auto-exhaust
firingCountnumber*Total firings so far
nextFireAtISO 8601Next scheduled fire time (null when paused)
expiresAtISO 8601Expiration timestamp
createdAtISO 8601*Creation timestamp
updatedAtISO 8601*Last modification timestamp

Fetch all schedules to build a scheduling dashboard — shows active, paused, and exhausted schedules:

// List schedules and show when each will next fire
const schedules: Schedule[] = await fetch('/api/schedules').then(r => r.json());

const active = schedules.filter(s => s.status === 'active');
active.forEach(s => {
const next = s.nextFireAt ? new Date(s.nextFireAt).toLocaleString() : 'none';
console.log(`${s.name} — next: ${next} (fired ${s.firingCount}x)`);
});

Example response:

[
  {
    "id": "sched-e7b2-1f4a",
    "name": "Daily standup summary",
    "prompt": "Review all open tasks, summarize blockers, and draft a standup message",
    "cronExpression": "0 9 * * 1-5",
    "projectId": "proj-8f3a-4b2c",
    "assignedAgent": "claude-code",
    "agentProfile": "project-manager",
    "type": "scheduled",
    "status": "active",
    "recurs": true,
    "firingCount": 14,
    "nextFireAt": "2026-04-04T09:00:00.000Z",
    "createdAt": "2026-03-15T10:00:00.000Z",
    "updatedAt": "2026-04-03T09:00:00.000Z"
  }
]

Create Schedule

POST /api/schedules

Create a new schedule. The interval field accepts natural language (e.g. 'every weekday at 9am'), shorthand ('5m', '2h', '1d'), or raw cron expressions. Heartbeat schedules require a checklist.

Request Body

FieldTypeReqDescription
namestring*Schedule name
promptstring*Task prompt (required for scheduled type, optional for heartbeat)
intervalstring*Interval expression: natural language, shorthand, or cron
projectIdstring (UUID)Project to associate with
assignedAgentenumAgent runtime for execution
agentProfilestringProfile ID for agent configuration
typeenumscheduled (default) or heartbeat
recursbooleanWhether to repeat (default: true)
maxFiringsnumberStop after N firings
expiresInHoursnumberAuto-expire after N hours
documentIdsstring[]Document UUIDs to attach as inputs

Heartbeat-specific Fields

FieldTypeReqDescription
heartbeatChecklistChecklistItem[]*Array of { id, instruction, priority } items (required for heartbeat type)
activeHoursStartnumber (0–23)Start of active hours window
activeHoursEndnumber (0–23)End of active hours window
activeTimezonestringIANA timezone for active hours (default: UTC)
heartbeatBudgetPerDaynumberDaily budget limit for heartbeat actions

Response 201 Created — The created schedule object

Errors: 400 — Validation failure, invalid interval, or runtime/profile incompatibility

Create a standard recurring schedule that fires every weekday morning:

// Create a schedule with natural language interval
const schedule: Schedule = await fetch('/api/schedules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  name: 'Daily standup summary',
  prompt: 'Review all open tasks, summarize blockers, and draft a standup message',
  interval: 'every weekday at 9am',
  projectId: 'proj-8f3a-4b2c',
  assignedAgent: 'claude-code',
  agentProfile: 'project-manager',
}),
}).then(r => r.json());

console.log(schedule.cronExpression); // "0 9 * * 1-5"
console.log(schedule.nextFireAt);     // "2026-04-04T09:00:00.000Z"

Create a heartbeat monitor with active hours and a daily budget — the agent evaluates a checklist and only acts when conditions warrant:

const heartbeat: Schedule = await fetch('/api/schedules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  name: 'Production health check',
  type: 'heartbeat',
  interval: '30m',
  assignedAgent: 'claude-code',
  agentProfile: 'devops-engineer',
  heartbeatChecklist: [
    { id: 'uptime', instruction: 'Check that all API endpoints return 200', priority: 1 },
    { id: 'errors', instruction: 'Check error rate is below 1%', priority: 1 },
    { id: 'latency', instruction: 'Check P95 latency is under 500ms', priority: 2 },
  ],
  activeHoursStart: 8,
  activeHoursEnd: 22,
  activeTimezone: 'America/New_York',
  heartbeatBudgetPerDay: 5.00,
}),
}).then(r => r.json());

Get Schedule

GET /api/schedules/{id}

Retrieve a single schedule with its parsed heartbeat checklist and firing history (child tasks).

Response 200 — Extended schedule object

Additional Fields

FieldTypeReqDescription
heartbeatChecklistChecklistItem[]Parsed checklist array (null for scheduled type)
firingHistoryTask[]*Child tasks created by this schedule, newest first

Errors: 404 — Schedule not found

Fetch a schedule with its firing history to see every task it has spawned:

// Get schedule detail with full firing history
const schedule: ScheduleDetail = await fetch('/api/schedules/sched-e7b2-1f4a')
.then(r => r.json());

console.log(`${schedule.name} — ${schedule.firingCount} firings`);

// Show recent firing results
schedule.firingHistory.slice(0, 5).forEach(task => {
console.log(`  [${task.status}] ${task.title} — ${new Date(task.createdAt).toLocaleDateString()}`);
});

Example response:

{
  "id": "sched-e7b2-1f4a",
  "name": "Daily standup summary",
  "cronExpression": "0 9 * * 1-5",
  "type": "scheduled",
  "status": "active",
  "firingCount": 14,
  "nextFireAt": "2026-04-04T09:00:00.000Z",
  "heartbeatChecklist": null,
  "firingHistory": [
    {
      "id": "task-c2a8-5e1d",
      "title": "Daily standup summary (run #14)",
      "status": "completed",
      "createdAt": "2026-04-03T09:00:00.000Z"
    },
    {
      "id": "task-a1b7-3f9c",
      "title": "Daily standup summary (run #13)",
      "status": "completed",
      "createdAt": "2026-04-02T09:00:00.000Z"
    }
  ]
}

Update Schedule

PATCH /api/schedules/{id}

Update schedule fields. Status changes follow strict transitions: only active can be paused, only paused can be resumed. Changing the interval recomputes the next fire time.

Request Body (all fields optional)

FieldTypeReqDescription
statusenumSet to paused or active
namestringUpdated name
promptstringUpdated prompt
intervalstringNew interval (recomputes next fire time)
assignedAgentstringNew agent runtime
agentProfilestringNew profile ID
heartbeatChecklistChecklistItem[]Updated checklist (heartbeat type)
activeHoursStartnumber | nullUpdated active hours start
activeHoursEndnumber | nullUpdated active hours end
activeTimezonestringUpdated timezone
heartbeatBudgetPerDaynumber | nullUpdated daily budget

Errors:

  • 400 — Invalid interval, empty name/prompt, or runtime/profile incompatibility
  • 404 — Schedule not found
  • 409 — Invalid status transition (e.g. pausing an already paused schedule)

Pause a schedule to temporarily stop it from firing, then resume later:

// Pause a schedule — nextFireAt becomes null
await fetch('/api/schedules/sched-e7b2-1f4a', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'paused' }),
});

// Later, resume it — nextFireAt is recomputed
await fetch('/api/schedules/sched-e7b2-1f4a', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'active' }),
});

Delete Schedule

DELETE /api/schedules/{id}

Permanently remove a schedule. Existing child tasks are not affected.

Response 200{ "deleted": true }

Errors: 404 — Schedule not found

// Delete a schedule — child tasks remain intact
await fetch('/api/schedules/sched-e7b2-1f4a', { method: 'DELETE' });

Parse Interval

POST /api/schedules/parse

Preview how an interval expression will be parsed. Returns the resolved cron expression, human-readable description, next 3 fire times, and confidence score. Useful for validating expressions before creating a schedule.

Request Body

FieldTypeReqDescription
expressionstring*Interval to parse: natural language, shorthand, or cron

Response Body

FieldTypeReqDescription
cronExpressionstring*Resolved cron expression
descriptionstring*Human-readable description
nextFireTimesstring[]*Next 3 fire times as ISO 8601
confidencenumber*Parse confidence (0.0–1.0)

Errors: 400 — Expression missing or unparseable

Test how different interval formats resolve — useful for building a schedule creation form with live preview:

// Preview different interval formats before creating a schedule
const formats: string[] = ['every weekday at 9am', '30m', '0 */4 * * *'];

for (const expression of formats) {
const result: ParseResult = await fetch('/api/schedules/parse', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ expression }),
}).then(r => r.json());

console.log(`"${expression}" → ${result.cronExpression}`);
console.log(`  ${result.description} (confidence: ${result.confidence})`);
console.log(`  Next: ${result.nextFireTimes[0]}`);
}

Example response:

{
  "cronExpression": "0 9 * * 1-5",
  "description": "At 09:00 AM, Monday through Friday",
  "nextFireTimes": [
    "2026-04-04T09:00:00.000Z",
    "2026-04-07T09:00:00.000Z",
    "2026-04-08T09:00:00.000Z"
  ],
  "confidence": 0.95
}

Get Heartbeat History

GET /api/schedules/{id}/heartbeat-history

Retrieve recent heartbeat evaluation history including both action and suppressed entries. Only available for heartbeat-type schedules. Returns up to 50 most recent entries.

Response Body

FieldTypeReqDescription
historyHistoryEntry[]*Array of heartbeat log entries
statsobject*Aggregate statistics for the heartbeat schedule

History Entry

FieldTypeReqDescription
idstring*Log entry ID
taskIdstring*Associated task ID
eventenum*heartbeat_action or heartbeat_suppressed
payloadobjectEvent-specific payload data
timestampISO 8601*When the event occurred

Stats Object

FieldTypeReqDescription
suppressionCountnumber*Total suppressions (no action taken)
lastActionAtISO 8601When the last action was taken
firingCountnumber*Total heartbeat evaluations
heartbeatSpentTodaynumber*Budget spent today
heartbeatBudgetPerDaynumberConfigured daily budget

Errors:

  • 400 — Not a heartbeat schedule
  • 404 — Schedule not found

Check heartbeat history to see how often the monitor takes action vs suppresses — useful for tuning checklist sensitivity:

// Review heartbeat history to tune monitoring sensitivity
const { history, stats }: HeartbeatHistory = await fetch('/api/schedules/sched-hb-3c9d/heartbeat-history')
.then(r => r.json());

// Calculate suppression ratio
const ratio: number = stats.suppressionCount / stats.firingCount;
console.log(`Action rate: ${((1 - ratio) * 100).toFixed(0)}%`);
console.log(`Budget: $${stats.heartbeatSpentToday.toFixed(2)} / $${stats.heartbeatBudgetPerDay?.toFixed(2) ?? 'unlimited'}`);

// Show recent entries
history.slice(0, 5).forEach(entry => {
const icon = entry.event === 'heartbeat_action' ? 'ACT' : 'SKIP';
console.log(`  [${icon}] ${new Date(entry.timestamp).toLocaleTimeString()}`);
});

Example response:

{
  "history": [
    {
      "id": "log-7a2b-4e1c",
      "taskId": "task-f8d3-9b7e",
      "event": "heartbeat_action",
      "payload": { "triggeredChecks": ["errors", "latency"] },
      "timestamp": "2026-04-03T14:30:00.000Z"
    },
    {
      "id": "log-5c1d-8f3a",
      "taskId": "task-a2e7-6c4b",
      "event": "heartbeat_suppressed",
      "payload": { "reason": "All checks passed" },
      "timestamp": "2026-04-03T14:00:00.000Z"
    }
  ],
  "stats": {
    "suppressionCount": 42,
    "lastActionAt": "2026-04-03T14:30:00.000Z",
    "firingCount": 48,
    "heartbeatSpentToday": 1.85,
    "heartbeatBudgetPerDay": 5.00
  }
}

Status Transitions

Schedules follow a strict lifecycle. Invalid transitions return 409.

FromAllowed Targets
activepaused
pausedactive
exhausted(terminal — no transitions)

Schedules move to exhausted automatically when firingCount reaches maxFirings or when expiresAt is passed.

Interval Formats

The interval field accepts three formats, tried in order:

FormatExampleDescription
Natural languageevery weekday at 9amParsed via NLP with confidence score
Shorthand5m, 2h, 1dMinutes, hours, or days
Cron expression0 9 * * 1-5Standard 5-field cron syntax

Use the /api/schedules/parse endpoint to preview how any expression will be interpreted before creating a schedule.

Error Format

All errors follow a consistent JSON format:

{
  "error": "Descriptive error message"
}