Recipes
Background agents (cron + webhooks)
Run agents on a schedule or in response to incoming webhooks, without pulling in a job-queue dependency.
Two primitives, zero extra deps:
createCronSchedulerparses a minimal 5-field cron (*/15 * * * *) plus anevery:<ms>shortcut.createWebhookHandlerreturns a framework-agnostic(req) => reshandler you can mount on Express, Hono, Next API routes, etc.
Both accept any AgentHandle (anything with name + run(task)),
so they compose with topologies
and durable runners.
Install
Ships with @agentskit/runtime.
Cron
import { createCronScheduler } from '@agentskit/runtime'
const scheduler = createCronScheduler({
jobs: [
{
schedule: '0 9 * * 1-5', // weekdays at 9am
agent: dailyDigestAgent,
task: now => `Generate digest for ${now.toISOString().slice(0, 10)}`,
},
{
schedule: 'every:60000', // every minute
agent: healthCheckAgent,
task: 'run health check',
},
],
onEvent: e => logger.info('[cron]', e),
})
scheduler.start()
// ...
scheduler.stop()For tests: inject a fake clock and drive ticks manually.
createCronScheduler({
jobs: [...],
now: () => fakeClock,
scheduleTick: fn => (cleanup = setInterval(fn, 100)),
})scheduler.tick(now?) fires every job whose schedule matches now.
Webhooks
import { createWebhookHandler } from '@agentskit/runtime'
const handler = createWebhookHandler({
agent: supportTriageAgent,
verify: req => req.headers?.['x-signature'] === expectedSignature,
extractTask: req => `Triage ticket: ${JSON.stringify(req.body)}`,
context: req => ({ tenantId: req.headers?.['x-tenant'] }),
})
// Express
app.post('/hooks/support', async (req, res) => {
const result = await handler({ headers: req.headers, body: req.body })
res.status(result.status).set(result.headers ?? {}).send(result.body)
})
// Hono
app.post('/hooks/support', async c => {
const result = await handler({ headers: Object.fromEntries(c.req.raw.headers), body: await c.req.json() })
return new Response(result.body, { status: result.status, headers: result.headers })
})Default extractor reads body.task for JSON, falls back to the raw
string. Default context is pass-through.
Pair with durable + HITL
- Wrap the agent's work in a
createDurableRunnerto survive crashes. - Pause risky side effects behind
createApprovalGate.
A webhook that kicks off a long-running durable flow looks like:
const handler = createWebhookHandler({
agent: {
name: 'onboarding',
async run(task, ctx) {
const runner = createDurableRunner({ store, runId: ctx.tenantId + ':' + ctx.userId })
await runner.step('welcome', () => sendWelcome(...))
await runner.step('provision', () => provision(...))
return 'ok'
},
},
})