> ## Documentation Index
> Fetch the complete documentation index at: https://headgate.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotently create or update a schedule.

> Re-upserting with an unchanged spec keeps the schedule's phase (next_run is not
reset); a changed spec re-anchors it. Specs are "@every:<ms>" (epoch-aligned) or
a cron expression with five fields, or six fields when second-level precision is needed.

A cron spec may carry a `CRON_TZ=<IANA zone>` prefix and is then evaluated on
that zone's LOCAL calendar — `CRON_TZ=America/New_York 0 9 * * *`. The zone
lives IN the spec string rather than in a field of its own, so changing the zone
is a changed spec and re-anchors the phase like any other edit. A local time
that does not exist (spring forward) is SKIPPED; one that occurs twice (fall
back) fires ONCE, at the first occurrence. "@every" is always epoch-aligned UTC
and REJECTS a `CRON_TZ=` prefix — an interval has no wall clock. An unknown
zone is a 400 here, never a surprise at fire time.

Invariant 16 applies to schedules too: the gate's periodic work is writable at
runtime, not baked into a deploy.

The enqueue authorizer evaluates the schedule's kind and payload before the
durable definition is written, preventing a future periodic authorization bypass.




## OpenAPI

````yaml /api/headgate.openapi.yaml put /periodic/{id}
openapi: 3.1.0
info:
  title: headgate control API
  version: 0.1.0
  description: >
    The web UI is one client of this control API and gets no

    privileged access — asynqmon reads Redis directly, which is why its
    compatibility

    note is three minor versions stale.


    Both the Go and Rust implementations serve this spec, and the conformance
    suite

    asserts identical responses. Every list endpoint is bounded to prevent
    inspection

    from becoming an unbounded store operation: asynq's

    GetQueueInfo is O(number of groups) and has pinned Redis CPU for seconds in

    production. Monitoring caused the outage.


    DERIVED FROM THE ARCHITECTURE, NOT FROM A PREDECESSOR'S UI. The first draft
    of this

    spec was the asynq Inspector surface -- list by state, act on one job, pause
    a queue

    -- with new nouns bolted on. That inherited three problems: it omitted bulk

    operations, history, and enqueue (which asynq and apalis-board respectively
    DO have),

    and more importantly it had no endpoint for the question this system's own
    design

    creates. When dequeue is an admission decision, the operator's first
    question is not

    "what is in the queue" but "why is THIS job not running" -- see
    /jobs/{id}/admission.

    No predecessor needs that endpoint because no predecessor has a gate.
servers:
  - url: /api/v1
security: []
paths:
  /periodic/{id}:
    put:
      summary: Idempotently create or update a schedule.
      description: >
        Re-upserting with an unchanged spec keeps the schedule's phase (next_run
        is not

        reset); a changed spec re-anchors it. Specs are "@every:<ms>"
        (epoch-aligned) or

        a cron expression with five fields, or six fields when second-level
        precision is needed.


        A cron spec may carry a `CRON_TZ=<IANA zone>` prefix and is then
        evaluated on

        that zone's LOCAL calendar — `CRON_TZ=America/New_York 0 9 * * *`. The
        zone

        lives IN the spec string rather than in a field of its own, so changing
        the zone

        is a changed spec and re-anchors the phase like any other edit. A local
        time

        that does not exist (spring forward) is SKIPPED; one that occurs twice
        (fall

        back) fires ONCE, at the first occurrence. "@every" is always
        epoch-aligned UTC

        and REJECTS a `CRON_TZ=` prefix — an interval has no wall clock. An
        unknown

        zone is a 400 here, never a surprise at fire time.


        Invariant 16 applies to schedules too: the gate's periodic work is
        writable at

        runtime, not baked into a deploy.


        The enqueue authorizer evaluates the schedule's kind and payload before
        the

        durable definition is written, preventing a future periodic
        authorization bypass.
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - spec
              properties:
                kind:
                  type: string
                spec:
                  type: string
                payload:
                  type: string
                  contentEncoding: base64
                queue:
                  type: string
                  default: default
                partition_key:
                  type: string
                rate_class:
                  type: string
                priority:
                  type: integer
                max_attempts:
                  type: integer
                retention_ms:
                  type: integer
                on_missed:
                  type: string
                  enum:
                    - skip
                    - run_once
                    - backfill
                backfill_limit:
                  type: integer
                paused:
                  type: boolean
      responses:
        '200':
          description: Upserted
        '400':
          description: Bad spec or policy
        '403':
          description: Enqueue kind forbidden
components:
  parameters:
    Id:
      name: id
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
      description: >
        Required on every mutating request. A double-clicked Retry, or a proxy
        retrying a

        POST, must not enqueue the job twice. Keys are retained long enough to
        cover a

        client retry window and the response is replayed verbatim.

````