DOCUMENTATION

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

TEXT
https://hypervize.tech/api/chronos

Billing (each run)

TEXT
$0.01 Chronos fee
+ catalog price of every tool step that runs
+ token pricing for every model step

Prepaid 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

HTTP
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

TermMeaning
Event / stepkind: "tool" or kind: "model", executed in order
RecipeOrdered steps to re-run (saved once, reusable)
JobRecipe + schedule + IANA timezone + on/off
RunOne 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.

JS
// 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

BASH
export HVZ_KEY="hvz_live_…"
JS
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.

JS
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

  1. Enable the tools you name (Pandora, Vesper, Herald, …) for your account in Alexandria or via /api/tools.
  2. Prepaid balance above $0 (runs also enforce a small floor).
  3. 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.

JS
// … 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:

JS
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.

TokenMeaning
{{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 assigns step_0, step_1, … in order—reference those.

Temporal auto-rewrite (create time)

At capture, every string arg (deep) is scanned for temporal artifacts:

ArtifactBecomes
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:

JS
await chronos.createJobFromEvents({
  // ...
  suggestTemporalBindings: false,
  events: [/* … */],
});

Preview capture (no schedule)

Inspect rewritten steps before you commit a job:

JS
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:

JS
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

JS
// 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 present

Run-now vs scheduled

How the run startsBehavior
POST …/run-nowSynchronous 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):

JS
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

JS
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

JS
/**
 * 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):

JS
{
  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):

JS
{ 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.

  1. Enable tools and complete a turn that calls them.
  2. Open Schedule on that assistant message.
  3. Preview shows captured tools in order. If the turn used Herald, you can re-compose the delivery body with a model step each fire.
  4. Manage jobs under Library → Scheduled tasks.

Programmatic jobs still use explicit events[] / toolCalls[] as in the examples above.


Endpoints

MethodPathPurpose
POST/api/chronos/jobsCreate job from events[], toolCalls[], or recipeId
GET/api/chronos/jobsList jobs
GET/api/chronos/jobs/:idJob detail
PATCH/api/chronos/jobs/:idUpdate name, enabled, schedule, timezone
DELETE/api/chronos/jobs/:idDelete job
POST/api/chronos/jobs/:id/run-nowRun immediately (billable)
GET/api/chronos/jobs/:id/runsRun history (?limit=)
GET/api/chronos/runs/:idSingle run
POST/api/chronos/capturePreview or persist a recipe
GET / POST/api/chronos/recipesList / create recipes

Limits

LimitValue
Min interval15 minutes
Max jobs per user25
Max steps per recipe20
Missed windowStart > 15 minutes late → skipped_stale (no Chronos fee)
Concurrent runs per user2

Errors

HTTP / codeTypical cause
401Missing or invalid API key / session
403 chronos_not_enabledChronos not enabled on your account (Alexandria / Chat tools)
403 chronos_disabledChronos is turned off for the platform right now
402 / balance errorsPrepaid balance too low
400 validation_errorMissing name, schedule, timezone, or events
404 NOT_FOUNDJob or run id not yours / missing
Tool / OAuth failuresStep fails at run time; see run.step_results and error_summary
JS
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

  1. Prepaid balance greater than $0
  2. Platform tools used in the recipe enabled for your account
  3. Chronos enabled for your account
  4. Connected accounts (Google, etc.) for tools that need OAuth

Was this helpful?Send feedback