Pulse — Scheduled Jobs/API Reference

Pulse API Reference


Handler signature

export default {
  async pulse(event, env) {
    // ...
  },
};

The pulse export is optional. If present, it is called for every scheduled tick. If absent, the Cell silently ignores any pulse schedules declared in ribo.toml.


The event object

Property Type Description
event.scheduledTime Date The intended fire time (UTC). This is the time the cron expression resolved to, not the actual dispatch time. Actual delivery may lag by up to ~30 seconds.
event.cron string The cron expression that triggered this pulse (e.g. "0 * * * *"). Useful when a Cell has multiple [[pulse]] schedules and needs to distinguish between them.

The env object

env is identical to the env passed to fetch. All bindings declared in ribo.toml — c3 databases, g7 buckets, text values — are available in the pulse handler.

async pulse(event, env) {
  // c3 — reads and writes work exactly as in fetch
  const { results } = await env.DB.prepare("SELECT COUNT(*) as n FROM jobs").all();

  // g7 — object storage is fully accessible
  await env.BUCKET.put("report.txt", buildReport(results));

  // outbound fetch — unrestricted
  await fetch("https://hooks.example.com/notify", {
    method: "POST",
    body: JSON.stringify({ fired: event.scheduledTime }),
  });
}

Return value

The return value of pulse is ignored. There is no HTTP response to produce. The function can return void, undefined, or a Promise of either. Unhandled exceptions are logged by the platform and do not affect subsequent pulse firings.


Distinguishing multiple schedules

If your Cell has more than one [[pulse]] block, use event.cron to branch:

async pulse(event, env) {
  switch (event.cron) {
    case "* * * * *":
      await runMinuteTask(env);
      break;
    case "0 * * * *":
      await runHourlyTask(env);
      break;
    case "0 0 * * *":
      await runDailyTask(env);
      break;
  }
}

Common patterns

Periodic cleanup

async pulse(event, env) {
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
  await env.DB.prepare("DELETE FROM events WHERE created_at < ?").bind(cutoff).run();
}

Aggregate and report

async pulse(event, env) {
  const { results } = await env.DB.prepare(
    "SELECT COUNT(*) as total FROM orders WHERE created_at > datetime('now', '-1 hour')"
  ).all();
  const count = results[0]?.total ?? 0;
  await fetch("https://monitoring.example.com/metric", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ metric: "orders_per_hour", value: count, ts: event.scheduledTime }),
  });
}

Sync from an external API

async pulse(event, env) {
  const res = await fetch("https://api.example.com/data");
  const items = await res.json();
  for (const item of items) {
    await env.DB.prepare(
      "INSERT OR REPLACE INTO items (id, name, updated_at) VALUES (?, ?, ?)"
    ).bind(item.id, item.name, new Date().toISOString()).run();
  }
}

Limitations

Best-effort delivery. If the Cell is unavailable or throws an unhandled error during a pulse, the event is not retried. Use c3 to record state and detect missed ticks if your use case requires reliability.

Minimum interval: 1 minute. Cron expressions with sub-minute intervals are not supported.

UTC only. All cron schedules are evaluated in UTC. There is no timezone support.

No waitUntil. The ctx argument is not passed to pulse. Long-running work must complete before the handler returns (or await it inline).


See also