Chronos (Scheduled Recipes)
Schedule ordered tool and model steps with your API key or Chat. Full JavaScript examples for programmatic use.
Chronos — Scheduled Recipes
Run an ordered sequence of platform tools and optional model steps on a schedule—digests, watches, recurring deliveries, and similar habits.
Who it’s for
- API / code — create and manage jobs with your Hypervize API key (same key as inference)
- Chat — Schedule from a completed reply that used tools, or manage jobs in Library → Scheduled tasks
Base URL
https://hypervize.tech/api/chronosBilling (each run)
$0.01 Chronos fee
+ catalog price of every tool step that runs
+ token pricing for every model stepPrepaid balance must stay above a small floor before a run starts. Failed mid-run steps still may have incurred tool or token charges for work already done.
Auth
Authorization: Bearer hvz_live_…Same key as Elastic Inference. Chat UI uses your session on the same routes.
Account opt-in: enable Chronos under Alexandria or Chat tools (same toggle as Athena and other platform tools). Until it is on, Schedule is hidden and Chronos API returns 403 chronos_not_enabled.
Platform flag: if Chronos is turned off for the whole platform, every Chronos route returns 403 chronos_disabled (distinct from account opt-in).
Concepts
| Term | Meaning |
|---|---|
| Event / step | kind: "tool" or kind: "model", executed in order |
| Recipe | Ordered steps to re-run (saved once, reusable) |
| Job | Recipe + schedule + IANA timezone + on/off |
| Run | One execution of a job (scheduled or run-now) |
| Bindings | {{…}} placeholders resolved at fire time |
You own the step order. Chronos replays the sequence you send—it does not invent tools from natural language.
Arguments that look like absolute timestamps (e.g. time_min: "2026-07-17T05:00:00.000Z") are rewritten at create time to day-relative bindings so each fire uses the correct local window.
JavaScript client (copy-paste)
Use this thin helper in Node 18+, Bun, Deno, or the browser. All examples below build on it.
// chronos-client.js
const BASE = "https://hypervize.tech/api/chronos";
/**
* @param {string} apiKey Hypervize key (hvz_…)
*/
export function createChronosClient(apiKey) {
if (!apiKey) throw new Error("HVZ_KEY required");
async function request(method, path, body) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
...(body != null ? { "Content-Type": "application/json" } : {}),
},
body: body != null ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(
data.error || data.message || `Chronos HTTP ${res.status}`,
);
err.status = res.status;
err.code = data.code;
err.body = data;
throw err;
}
return data;
}
return {
/** Create job + recipe from ordered events (preferred). */
createJobFromEvents: (body) => request("POST", "/jobs", body),
/** List jobs for this account. */
listJobs: () => request("GET", "/jobs"),
/** Job detail. */
getJob: (jobId) => request("GET", `/jobs/${jobId}`),
/** Update name, enabled, schedule, and/or timezone. */
updateJob: (jobId, patch) => request("PATCH", `/jobs/${jobId}`, patch),
/** Delete a job. */
deleteJob: (jobId) => request("DELETE", `/jobs/${jobId}`),
/** Billable immediate run. Returns { runId, status, error_summary? }. */
runNow: (jobId) => request("POST", `/jobs/${jobId}/run-now`),
/** Recent runs for a job (limit 1–50, default 20). */
listRuns: (jobId, limit = 20) =>
request("GET", `/jobs/${jobId}/runs?limit=${limit}`),
/** Single run detail (step results, status, previews). */
getRun: (runId) => request("GET", `/runs/${runId}`),
/**
* Preview capture (default) or persist a recipe without a schedule.
* Body: { events } or { toolCalls }; set persist: true + name to save.
*/
capture: (body) => request("POST", "/capture", body),
listRecipes: () => request("GET", "/recipes"),
/** Create recipe from explicit steps[]. */
createRecipe: (body) => request("POST", "/recipes", body),
/** Attach an existing recipe to a new schedule. */
createJobFromRecipe: (body) => request("POST", "/jobs", body),
};
}Env
export HVZ_KEY="hvz_live_…"import { createChronosClient } from "./chronos-client.js";
const chronos = createChronosClient(process.env.HVZ_KEY);Quickstart: weekday briefing job
Creates a recipe and a job in one call: gather → model compose → Herald email.
import { createChronosClient } from "./chronos-client.js";
const chronos = createChronosClient(process.env.HVZ_KEY);
const { job, recipe, bindingSuggestions } = await chronos.createJobFromEvents({
name: "weekday-digest",
timezone: "America/Chicago",
// omit enabled or set true; false keeps the job paused
enabled: true,
schedule: {
kind: "weekly_times",
days: ["MON", "TUE", "WED", "THU", "FRI"],
time: "08:30", // local wall clock HH:mm (24h)
},
// Ordered steps — this is the entire recipe
events: [
{
kind: "tool",
// optional stable id (auto step_0, step_1, … if omitted)
id: "cal",
name: "pandora_list_calendar_events",
arguments: {
// Absolute ISO is rewritten → {{run.day_start_iso}} at capture time
time_min: "2026-01-01T00:00:00.000Z",
max_results: 25,
},
},
{
kind: "tool",
id: "mail",
name: "pandora_search_gmail",
arguments: {
query: "is:unread newer_than:1d",
max_results: 10,
},
},
{
kind: "tool",
id: "news",
name: "vesper",
arguments: {
query: "top world and business news last 24 hours",
num_results: 5,
instructions: "List title, url, one-line summary for each.",
},
},
{
kind: "model",
id: "compose",
// Instruction for the model each fire; prior tool results are in context
user_instruction:
"Write a short HTML briefing for an executive: calendar, inbox, news. Be concise.",
},
{
kind: "tool",
id: "send",
name: "herald",
arguments: {
format: "html",
subject: "Digest — {{run.local_date}}",
// Wire model output into delivery
body: "{{steps.compose.result}}",
},
},
],
});
console.log("job id:", job.id);
console.log("recipe id:", recipe.id);
console.log("next_run_at:", job.next_run_at);
console.log("temporal rewrites:", bindingSuggestions);
// e.g. time_min → {{run.day_start_iso}}Requirements before this works
- Enable the tools you name (Pandora, Vesper, Herald, …) for your account in Alexandria or via
/api/tools. - Prepaid balance above $0 (runs also enforce a small floor).
- OAuth connections (e.g. Google for Pandora) completed where needed.
Optional: PDF (or CSV) with Ledger
Enable Ledger as well. Chronos runs the same tools as Chat—add a Ledger step after compose to save a PDF/CSV to Library, then deliver with Herald (body and/or attachment). Build or capture the recipe in Chat so arguments and any attachment file id are filled the way you want.
// … after compose model step …
{
kind: "tool",
id: "file",
name: "ledger",
arguments: {
action: "create",
format: "pdf",
title: "Digest — {{run.local_date}}",
content: "{{steps.compose.result}}",
filename: "digest.pdf",
},
},
// Herald step: short email body; attach the Library file when you have its file_id
// (from a prior Chat capture or Ledger result). See Platform Tools → Ledger + Herald.See Platform Tools → Ledger for parameters, Library downloads, API examples, and Herald attachment_file_id.
Tool-only jobs (toolCalls[])
When you have no model step, you can pass toolCalls instead of events:
const { job } = await chronos.createJobFromEvents({
name: "hourly-time-ping",
timezone: "UTC",
schedule: {
kind: "cron",
expr: "0 * * * *", // every hour at :00 — min interval is 15 minutes
},
toolCalls: [
{ name: "athena", arguments: {} },
{
name: "herald",
arguments: {
format: "text",
subject: "Chronos ping {{run.local_date}}",
body: "Athena says: {{steps.step_0.result}}",
},
},
],
});Prefer events[] when you need a model step between tools.
Bindings
Tokens appear anywhere in string arguments and are resolved at run time. Unresolved tokens fail the step.
| Token | Meaning |
|---|---|
{{run.timezone}} | Job IANA timezone |
{{run.local_date}} | Local calendar date (YYYY-MM-DD) for the fire |
{{run.day_start_iso}} | Start of that local day (ISO UTC) |
{{run.day_end_iso}} | End of that local day (ISO UTC) |
{{run.now_iso}} | Instant the run started |
{{run.scheduled_for_iso}} | Scheduled fire time |
{{steps.<id>.result}} | Full text result of an earlier step |
Step ids
- If you set
"id": "compose"on an event, use{{steps.compose.result}}. - If you omit
id, Chronos assignsstep_0,step_1, … in order—reference those.
Temporal auto-rewrite (create time)
At capture, every string arg (deep) is scanned for temporal artifacts:
| Artifact | Becomes |
|---|---|
| Whole-value ISO instant on start/min/since/from-like keys | {{run.day_start_iso}} |
| Whole-value ISO instant on end/max/until/to-like keys | {{run.day_end_iso}} |
| Other whole-value ISO instants | {{run.now_iso}} (or day bounds via time-of-day heuristics) |
Whole-value YYYY-MM-DD | {{run.local_date}} |
Embedded dates in any string (digest-2026-07-21.pdf, July 22, 2026, slash forms, compact YYYYMMDD) | {{run.local_date}} |
Disable with:
await chronos.createJobFromEvents({
// ...
suggestTemporalBindings: false,
events: [/* … */],
});Preview capture (no schedule)
Inspect rewritten steps before you commit a job:
const preview = await chronos.capture({
events: [
{
kind: "tool",
name: "pandora_list_calendar_events",
arguments: {
time_min: "2026-07-17T05:00:00.000Z",
max_results: 10,
},
},
{
kind: "model",
user_instruction: "Summarize today's calendar in 5 bullets.",
},
],
});
console.log(preview.steps);
console.log(preview.bindingSuggestions);
// { stepId, argKey, from, to }[]Persist a recipe without scheduling:
const { recipe } = await chronos.capture({
name: "calendar-summary-recipe",
persist: true,
events: [
{
kind: "tool",
id: "cal",
name: "pandora_list_calendar_events",
arguments: { time_min: "2026-01-01T00:00:00.000Z" },
},
{
kind: "model",
id: "compose",
user_instruction: "Summarize the calendar for email.",
},
],
});
// Later: attach a schedule to an existing recipe
const { job } = await chronos.createJobFromRecipe({
name: "morning calendar",
recipeId: recipe.id,
timezone: "America/New_York",
schedule: {
kind: "weekly_times",
days: ["MON", "TUE", "WED", "THU", "FRI"],
time: "07:00",
},
});Run now, list runs, inspect a run
// Fire immediately (billable — same fee + tools + tokens as a scheduled fire)
const result = await chronos.runNow(job.id);
console.log(result);
// {
// runId: "…",
// status: "succeeded" | "failed" | "skipped_stale" | …,
// error_summary?: "…"
// }
// History
const { runs } = await chronos.listRuns(job.id, 10);
for (const r of runs) {
console.log(r.id, r.status, r.scheduled_for, r.chronos_fee_cents);
}
// Detail (step previews, errors)
const { run } = await chronos.getRun(result.runId);
console.log(run.status);
console.log(run.step_results);
// [
// { id, name, kind, status, duration_ms, error?, result_preview? },
// …
// ]
console.log(run.error_summary);
console.log(run.html_body); // last step text artifact when presentRun-now vs scheduled
| How the run starts | Behavior |
|---|---|
POST …/run-now | Synchronous HTTP — the request waits until the run finishes and returns { runId, status, error_summary? }. |
| Scheduled fire (cron / weekly_times) | Runs in the background. Use listRuns / getRun after next_run_at to inspect status and step results. |
Optional helper if you still want to poll a run id (e.g. after a scheduled fire, or if you only stored runId):
async function waitForRun(chronos, runId, { timeoutMs = 120_000 } = {}) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const { run } = await chronos.getRun(runId);
if (
run.status === "succeeded" ||
run.status === "failed" ||
run.status === "skipped_stale"
) {
return run;
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Timed out waiting for Chronos run");
}List, update, pause, delete
const { jobs } = await chronos.listJobs();
for (const j of jobs) {
console.log(j.id, j.name, j.enabled, j.next_run_at, j.last_run_status);
}
const { job: detail } = await chronos.getJob(jobs[0].id);
// Pause
await chronos.updateJob(detail.id, { enabled: false });
// Resume + new time
await chronos.updateJob(detail.id, {
enabled: true,
timezone: "America/Los_Angeles",
schedule: {
kind: "weekly_times",
days: ["MON", "WED", "FRI"],
time: "09:15",
},
});
// Rename
await chronos.updateJob(detail.id, { name: "Team briefing" });
// Delete (removes the job; recipe may remain if shared)
await chronos.deleteJob(detail.id);Full end-to-end script
/**
* e2e-chronos.mjs
*
* export HVZ_KEY=hvz_live_…
* node e2e-chronos.mjs
*/
import { createChronosClient } from "./chronos-client.js";
const chronos = createChronosClient(process.env.HVZ_KEY);
async function main() {
// 1) Create
const { job, bindingSuggestions } = await chronos.createJobFromEvents({
name: `demo-${Date.now()}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
enabled: true,
schedule: {
kind: "weekly_times",
days: ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"],
time: "08:30",
},
events: [
{
kind: "tool",
id: "time",
name: "athena",
arguments: {},
},
{
kind: "model",
id: "compose",
user_instruction:
"One short paragraph: what time is it according to the tool result? Plain text.",
},
{
kind: "tool",
id: "send",
name: "herald",
arguments: {
format: "text",
subject: "Chronos demo {{run.local_date}}",
body: "{{steps.compose.result}}",
},
},
],
});
console.log("Created job", job.id);
console.log("Bindings applied:", bindingSuggestions);
// 2) Run immediately (does not wait for next_run_at)
const { runId, status, error_summary } = await chronos.runNow(job.id);
console.log("run-now:", { runId, status, error_summary });
const { run } = await chronos.getRun(runId);
console.log(
"steps:",
(run.step_results || []).map((s) => `${s.name}:${s.status}`).join(" → "),
);
// 3) Pause so it does not fire again while you experiment
await chronos.updateJob(job.id, { enabled: false });
console.log("Paused job", job.id);
// 4) Optional cleanup
// await chronos.deleteJob(job.id);
}
main().catch((e) => {
console.error(e.status, e.code, e.message, e.body);
process.exit(1);
});Schedules
Weekly + local time (recommended):
{
kind: "weekly_times",
days: ["MON", "TUE", "WED", "THU", "FRI"], // also SAT, SUN
time: "08:30", // HH:mm 24h in the job timezone
}Always set an IANA timezone on the job (e.g. America/Chicago, Europe/London).
Cron (5-field: minute hour day-of-month month day-of-week):
{ kind: "cron", expr: "30 8 * * 1-5" }Minimum interval between fires is 15 minutes. Jobs that start more than 15 minutes late are marked skipped_stale (no Chronos fee).
Chat Schedule
In Chat, Schedule appears on completed assistant replies that used tools. It builds a job from the tools that reply actually ran—not from a guessed product list.
- Enable tools and complete a turn that calls them.
- Open Schedule on that assistant message.
- Preview shows captured tools in order. If the turn used Herald, you can re-compose the delivery body with a model step each fire.
- Manage jobs under Library → Scheduled tasks.
Programmatic jobs still use explicit events[] / toolCalls[] as in the examples above.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/chronos/jobs | Create job from events[], toolCalls[], or recipeId |
GET | /api/chronos/jobs | List jobs |
GET | /api/chronos/jobs/:id | Job detail |
PATCH | /api/chronos/jobs/:id | Update name, enabled, schedule, timezone |
DELETE | /api/chronos/jobs/:id | Delete job |
POST | /api/chronos/jobs/:id/run-now | Run immediately (billable) |
GET | /api/chronos/jobs/:id/runs | Run history (?limit=) |
GET | /api/chronos/runs/:id | Single run |
POST | /api/chronos/capture | Preview or persist a recipe |
GET / POST | /api/chronos/recipes | List / create recipes |
Limits
| Limit | Value |
|---|---|
| Min interval | 15 minutes |
| Max jobs per user | 25 |
| Max steps per recipe | 20 |
| Missed window | Start > 15 minutes late → skipped_stale (no Chronos fee) |
| Concurrent runs per user | 2 |
Errors
| HTTP / code | Typical cause |
|---|---|
401 | Missing or invalid API key / session |
403 chronos_not_enabled | Chronos not enabled on your account (Alexandria / Chat tools) |
403 chronos_disabled | Chronos is turned off for the platform right now |
402 / balance errors | Prepaid balance too low |
400 validation_error | Missing name, schedule, timezone, or events |
404 NOT_FOUND | Job or run id not yours / missing |
| Tool / OAuth failures | Step fails at run time; see run.step_results and error_summary |
try {
await chronos.runNow(jobId);
} catch (e) {
console.error(e.status, e.code, e.message);
// e.body may include structured fields from the API
}Requirements
- Prepaid balance greater than $0
- Platform tools used in the recipe enabled for your account
- Chronos enabled for your account
- Connected accounts (Google, etc.) for tools that need OAuth
Related
- Platform Tools — Athena, Vesper, Herald, Pandora, Iris, …
- Billing & Payments — prepaid Chronos fee + tools + model tokens
- Tool Calling
- Authentication
- Code Examples (inference completions)
- Errors & Troubleshooting
- Dashboard: Access Keys · Usage (Chronos / tool filters)