Files

6.3 KiB

phase, title, status, priority, effort, dependencies
phase title status priority effort dependencies
2 Worker handler + fire client completed P1 2-3h
1

Phase 2: Worker handler + fire client

Overview

Implement the scheduled handler in plain JS with JSDoc type annotations: render text template, POST to /fire, log structured JSON. Single worker.js file. No fetch handler (per locked decision: scheduled-only).

Requirements

  • Functional:
    • On cron tick, POSTs to ROUTINE_FIRE_URL with bearer token, beta header, JSON body.
    • Logs claude_code_session_url on 2xx; logs status + body on non-2xx; never throws (would crash CF retry).
    • Renders TEXT_TEMPLATE with {LocalTime}, {Cron}, {ISO} placeholders.
    • No retry (each /fire = new session).
  • Non-functional:
    • Token never logged.
    • Use console.log(JSON.stringify({...})) for structured logs (CF picks these up).
    • JSDoc @typedef Env for environment bindings + @param/@returns on all exports.
    • Total file ≤ 200 lines (split if exceeds).

Architecture

ScheduledEvent (cron, scheduledTime)
    │
    ▼
fireRoutine(env, controller)
    ├── renderText(template, vars)         # simple {Tok} substitution
    ├── fetch(URL, POST, headers, body)
    └── log(json)                          # success or error

Mapping from Go daemon

Go (claude-code-routine-cron) JavaScript (this worker)
Config.FireURL env env.ROUTINE_FIRE_URL secret
Config.Token env env.ROUTINE_FIRE_TOKEN secret
Config.Schedules env wrangler.toml [triggers].crons (literal)
Config.Location (TZ) env.TZ plain var, default UTC
Config.Template (Go text/template) env.TEXT_TEMPLATE plain string + {Tok} substitution
FireClient.Fire() fireRoutine() function
cron.New() scheduler CF Workers scheduled handler (built-in)
slog JSON logs console.log(JSON.stringify(...))

JSDoc typedefs

/**
 * @typedef {object} Env
 * @property {string} ROUTINE_FIRE_URL  - secret: Anthropic /fire endpoint
 * @property {string} ROUTINE_FIRE_TOKEN - secret: per-routine bearer token
 * @property {string} [TEXT_TEMPLATE]   - plain var: prompt template, supports {LocalTime} {Cron} {ISO}
 * @property {string} [TZ]              - plain var: IANA tz, default 'UTC'
 */

/**
 * @typedef {object} FireResponse
 * @property {string} type
 * @property {string} claude_code_session_id
 * @property {string} claude_code_session_url
 */

Headers (verbatim from siblings)

Authorization: Bearer ${token}
anthropic-version: 2023-06-01
anthropic-beta: experimental-cc-routine-2026-04-01
Content-Type: application/json

Body

{ "text": "<rendered template>" }

Template substitution (KISS — not Go text/template)

Token Value
{ISO} new Date(scheduledTime).toISOString()
{LocalTime} formatted via Intl.DateTimeFormat(env.TZ ?? 'UTC')
{Cron} controller.cron

Default template: Scheduled trigger at {LocalTime} — same default as Go daemon.

  • Create: worker.js (export default { scheduled }, JSDoc-annotated)
  • (Optional split if >200 lines) fire-client.js, template-renderer.js

Implementation Steps

  1. Top of worker.js: write JSDoc @typedef Env + @typedef FireResponse blocks.
  2. Implement renderText(template, vars) with JSDoc:
    /**
     * @param {string} template
     * @param {Record<string, string>} vars
     * @returns {string}
     */
    
    Replaces {Key} tokens, leaves unknown tokens intact.
  3. Implement formatLocalTime(date, tz) using Intl.DateTimeFormat. JSDoc: @param {Date} date, @param {string} tz, @returns {string}.
  4. Implement fireRoutine(env, controller) with JSDoc @param {Env} env, @param {ScheduledController} controller, @returns {Promise<void>}:
    • Build text via renderText.
    • fetch(env.ROUTINE_FIRE_URL, { method: 'POST', headers, body: JSON.stringify({text}) }).
    • On !response.ok: console.log(JSON.stringify({level:'error', cron, status, body})).
    • On ok: parse JSON, log {level:'info', cron, session_url, session_id}.
    • Wrap in try/catch — log network errors, don't rethrow.
  5. Export default with JSDoc @type {ExportedHandler<Env>}:
    /** @type {ExportedHandler<Env>} */
    export default {
      async scheduled(controller, env, ctx) {
        ctx.waitUntil(fireRoutine(env, controller));
      },
    };
    
  6. npx tsc --noEmit --allowJs --checkJs worker.js --target es2022 --module esnext --moduleResolution bundler --types @cloudflare/workers-types (one-shot smoke check; not added to scripts to avoid TS dependency creep). Optional — skip if undesired.
  7. Local smoke: wrangler dev --test-scheduled, then curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*" — confirm log line.

Success Criteria

  • worker.js ≤ 200 lines (split if needed)
  • All exported functions have JSDoc with @param + @returns
  • wrangler deploy --dry-run validates module
  • Token never appears in logged output (manual grep on wrangler dev output)
  • Local __scheduled test produces structured JSON log

Risk Assessment

  • Intl.DateTimeFormat TZ support on Workers runtime: supported since 2023; verify with smoke test.
  • ctx.waitUntil vs awaiting in scheduled: waitUntil ensures fire completes even if handler returns; preferred per CF docs.
  • No compile-time type errors: JSDoc + editor LSP catches most; runtime tests (phase 4) catch the rest.
  • Template injection: text is sent only to Anthropic API; no XSS / SQL surface — safe.

Security Considerations

  • Token comes only from env.ROUTINE_FIRE_TOKEN (CF Secret), never bundled.
  • console.log body must not include Authorization header value.
  • TLS to api.anthropic.com is automatic (Workers fetch validates certs).