agentskit.js
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:

  • createCronScheduler parses a minimal 5-field cron (*/15 * * * *) plus an every:<ms> shortcut.
  • createWebhookHandler returns a framework-agnostic (req) => res handler 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 createDurableRunner to 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'
    },
  },
})

See also

✎ Edit this page on GitHub·Found a problem? Open an issue →·How to contribute →

On this page