Autonomous work
Run schedules, signed alerts, and change-detection loops without hidden cron or immortal in-process timers.
Autonomous work should survive restarts and remain attributable to the user or installation that created it. Noir separates the durable records from the runner that claims them.
Schedules
Import schedule primitives from @noir-agent/agent/scheduler. schedulerCapability() exposes owner-scoped schedule tools. A schedule may run once, at an interval, or from a timezone-aware cron expression.
import {
createSchedulerRunner,
postgresScheduleStore,
schedulerCapability,
} from '@noir-agent/agent/scheduler'
const store = postgresScheduleStore(process.env.DATABASE_URL!)
await store.migrate()
const schedules = schedulerCapability({
store,
presets: {
daily_brief: {
prompt: 'Summarize yesterday and identify one action.',
quiet: 'on-success',
},
},
})Presets keep trusted prompts and policy in code. Model-visible tools select a preset and timing; they do not submit arbitrary privileged prompts or owner IDs.
createSchedulerRunner() is deliberately separate. Call its bounded tick from your worker, queue, platform scheduler, or application loop. Noir does not start a module-level interval.
Delivery guarantees
- due occurrences are claimed with a lease and fencing token
- one occurrence ID is stable across retries
- misfire policy decides what happens after downtime
- quiet runs may suppress routine success, not failures
- run history records the actual terminal outcome
- a worker may heartbeat, complete, or release its claim
Signed alerts
Use @noir-agent/agent/alerts when an external system should wake an agent. alertsCapability() maps a configured alert name to one durable inbound event. Signatures bind the timestamp, event ID, and raw request body.
Reject stale timestamps before enqueueing work. Keep the signing secret in host code through an AlertSecretResolver; never expose it to the model or store it in an alert payload. Duplicate event IDs must map to one inbound job.
Change snapshots
Use @noir-agent/agent/snapshots for a loop that should speak only when a meaningful value changes. A snapshot record stores a bounded validated baseline. validateSnapshotCandidate() rejects empty, clipped, wrapper-shaped, or implausibly replaced candidates before they become the new truth.
Snapshots are not a general cache. Store the smallest comparison value, keep the user-facing evidence elsewhere, and use snapshotSetDiff() when set membership is the actual signal.
Production checklist
- use Postgres stores when work must survive a process restart
- make runner concurrency smaller than downstream provider limits
- heartbeat work that can exceed one lease interval
- pass the occurrence or event ID into downstream idempotency keys
- keep reconciliation bounded and repeat ticks until no due work remains
- alert on terminal failures and repeated misfires
- test a restart after claim and before completion
The scheduler owns durable timing. Your application still owns where and how often the runner is invoked.