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

# Workflow schedules and goals

> Dynamic definitions, schedules that start runs on a cadence, goals that repeat a workflow until a judge is satisfied, workspaces, and organization-wide reads

These routes create the things that start workflow [runs](/concepts/workflows) without a caller: a compose draft the agent can run, a schedule that fires on a cadence, and a goal that repeats a workflow until a judge says the objective is met. They also read a run's Job-tier workspace and step view, and list runs across an organization. Composing, scheduling, and creating or editing goals need `workflows:write` on the agent; pausing, resuming, and closing goals and releasing a workspace need `workflows:execute`; reads need `workflows:read`. Run routes, recovery, and the [error codes](/reference/rest/workflows#error-codes) are on [Workflows](/reference/rest/workflows), which also holds the base URL, scopes, and envelope pointer.

*Verified against lua-cli 3.33.0.*

## Compose

### POST /workflows/:agentId/compose

Validates a dynamic definition and, unless `dryRun`, saves it as a draft that `POST .../runs` can start by `composeId`. This is what the agent's own compose tool calls.

<ParamField body="form" type="string" required>`graph` or `script`.</ParamField>
<ParamField body="graph" type="object">The graph document, for `form: "graph"`.</ParamField>
<ParamField body="script" type="string">The script source (at most 256 KB), for `form: "script"`.</ParamField>
<ParamField body="input" type="any">A sample input to validate against.</ParamField>
<ParamField body="name" type="string">Save under this name: lowercase kebab-case, at most 64 characters.</ParamField>
<ParamField body="dryRun" type="boolean">Validate only; write no draft.</ParamField>
<ParamField body="threadId" type="string">The originating chat thread, when composed from chat.</ParamField>

**Response**

`200` with the compose result; validation problems are returned in `issues[]`, never as a `4xx`. `400 VALIDATION_FAILED` for a malformed body; `422 COMPOSE_FORM_FORBIDDEN` when the organization allows one form only; `503 CONTROL_UNAVAILABLE`.

## Schedules

A schedule is a job that starts a workflow on a cadence. Each schedule row is `{ jobId, workflowId, trigger: [{ type, expression?, executeAt?, seconds?, timezone?, triggerId? }], status, paused, nextRunAt, lastFiredAt?, consecutiveFailures, autoDisabled?, lastError?, idempotencyKeyTemplate?, goalId? }` with `status` one of `active`, `paused`, `inactive`, `completed`, `missed`, and `nextRunAt` `null` unless the schedule is live. A schedule that fails on consecutive fires is disabled by the platform and reports `autoDisabled { at, consecutiveFailures, lastRunId?, lastReason }`.

### GET /workflows/:agentId/schedules

Lists the agent's schedules as `{ "items": [...] }`.

Equivalent: `lua workflows schedules list`.

### GET /workflows/:agentId/schedules/:jobId

Returns one schedule. `404 SCHEDULE_NOT_FOUND`, also for a job that is not a workflow schedule.

### POST /workflows/:agentId/schedules

Creates a schedule, or replaces the one for the same workflow.

<ParamField body="workflowId" type="string" required>The definition; must be on this agent (`400 WORKFLOW_NOT_ON_AGENT`).</ParamField>

<ParamField body="schedules" type="array" required>
  From 1 to 5 slots of `{ type: "cron" | "once" | "interval", expression?, executeAt?, seconds?, timezone?, triggerId? }`: a 5-field cron expression with an IANA `timezone`, an ISO 8601 instant, or whole minutes of at least 60 seconds. A sixth slot is `409 SCHEDULE_CAP`.
</ParamField>

<ParamField body="name" type="string">Display name, at most 120 characters; defaults to the workflow name.</ParamField>
<ParamField body="input" type="any">The run input on every fire; frozen at create.</ParamField>
<ParamField body="budget" type="object">`{ maxCredits?, maxSteps?, maxDurationSeconds? }` per run.</ParamField>
<ParamField body="notify" type="string">`emailApp`, `email`, `app`, or `off`.</ParamField>
<ParamField body="workflowVersionId" type="string">Pin a version.</ParamField>
<ParamField body="correlationKeyTemplate" type="string">Rendered per fire, for example with `{{occurrenceIso}}`; at most 128 characters.</ParamField>

<ParamField body="idempotencyKeyTemplate" type="string">
  Rendered per fire into the run's idempotency key from `${input.<path>}` and `${scheduledTime}` (at most 128 characters); a fire whose key is still held replays that run instead of starting one. Refused with `400 VALIDATION_FAILED` when it has no placeholder or does not render against the input.
</ParamField>

<ParamField body="tags" type="string[]">At most 10 of at most 40 characters.</ParamField>
<ParamField body="initialState" type="object">Seeds every run.</ParamField>
<ParamField body="backfillOnEnable" type="object">`{ maxOccurrences }`: fire missed occurrences when re-enabled.</ParamField>

**Response**

`201` with `{ "jobId", "workflowId", "schedules", "nextRunAt", "notify" }`. `400 VALIDATION_FAILED` with `schedules-required` or `cadence-invalid` at `/schedules/<i>/<field>`, or `WORKFLOW_NOT_ON_AGENT`; `404 WORKFLOW_NOT_FOUND`; `409 SCHEDULE_CAP`; `503 SCHEDULE_PROVISION_FAILED`.

Equivalent: `lua workflows schedules create <workflow> --cadence '0 9 * * 1' --timezone Europe/London`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows schedules create lead-outreach --cadence '0 9 * * 1' --timezone Europe/London \
    --input '{"segment":"trial"}' --notify app
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/schedules', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({
      workflowId: '<<WORKFLOW_ID>>',
      schedules: [{ type: 'cron', expression: '0 9 * * 1', timezone: 'Europe/London' }],
      input: { segment: 'trial' },
      notify: 'app',
    }),
  });
  const schedule: { jobId: string; nextRunAt?: number | string | null } = await response.json();
  console.log(schedule.jobId, schedule.nextRunAt);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/schedules" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{
      "workflowId": "<<WORKFLOW_ID>>",
      "schedules": [{ "type": "cron", "expression": "0 9 * * 1", "timezone": "Europe/London" }],
      "input": { "segment": "trial" },
      "notify": "app"
    }'
  ```
</CodeGroup>

### PATCH /workflows/:agentId/schedules/:jobId

Pauses or resumes a schedule and sets its backfill options.

<ParamField body="paused" type="boolean">`false` re-enables the schedule.</ParamField>
<ParamField body="backfillOnEnable" type="object or null">`{ maxOccurrences }` to persist; `null` to clear.</ParamField>
<ParamField body="backfillNow" type="boolean">One-shot backfill on this re-enable; needs `paused: false` in the same body (`400 VALIDATION_FAILED` with `backfill-now-requires-enable` otherwise).</ParamField>
<ParamField body="idempotencyKeyTemplate" type="string or null">Set or clear the fire-time template; refused on a goal's cadence job.</ParamField>

**Response**

`200` with `{ "jobId", "workflowId", "status", "backfillOnEnable"?, "idempotencyKeyTemplate"?, "backfill"? }`. `400 VALIDATION_FAILED`; `404 SCHEDULE_NOT_FOUND`.

Equivalent: `lua workflows schedules pause <jobId>`, `lua workflows schedules resume <jobId> --backfill-now`.

### DELETE /workflows/:agentId/schedules/:jobId

Deletes a schedule and answers `{ "success": true }`. `404 SCHEDULE_NOT_FOUND`; `409 GOAL_SCHEDULE` with `goalId` for a goal's cadence, which is stopped by pausing or closing the goal.

Equivalent: `lua workflows schedules delete <jobId> --yes`.

## Goals

A goal runs a workflow repeatedly until a judge says the objective is met, within `maxRuns` and an optional credit cap. A goal is `{ goalId, orgId, agentId, workflowId, workflowVersionId?, objective, judge, cadence, evaluation, maxRuns, budget?, maxTotalCredits?, initialState?, input?, status, runsUsed, iteration, currentRunId?, lineageId, jobId?, lastRunId?, verdict?, pauseReason?, consecutiveContinues, createdBy, createdAt, updatedAt, doneAt?, closedBy?, closeNote?, note?, raises?, rearmed?, job? }` with `status` one of `active`, `paused`, `done`, `closed`, and `pauseReason` one of `user`, `budget`, `max_runs`, `strikes`.

### POST /workflows/:agentId/goals

Creates a goal.

<ParamField body="workflowId" type="string" required>The definition; must be on this agent.</ParamField>
<ParamField body="objective" type="string" required>At most 2,000 characters.</ParamField>

<ParamField body="judge" type="object" required>
  Either a predicate judge, `{ predicate: { path, op, value? } }`, where `path` is a dotted path (at most 256 characters) into the finished run (`output`, `state`, `steps.<stepId>`, `iteration`, `runStatus`) and `op` is one of `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `exists`, `truthy`; or an agent judge, `{ agentId, schema, role? }`, where `agentId` is a sub-agent id or `$self`, `schema` is a JSON Schema with a boolean `done` at the root, and `role` (`{ name, instructions, tools }`) is required for `$self`.
</ParamField>

<ParamField body="maxRuns" type="integer" required>From 1 to 100.</ParamField>
<ParamField body="cadence" type="array">At most 5 schedule slots in the schedule grammar; empty means `evaluation.mode: "immediate"`.</ParamField>
<ParamField body="evaluation" type="object">`{ mode: "immediate" | "cadence", delaySeconds? }`; `delaySeconds` only with `immediate`.</ParamField>
<ParamField body="budget" type="object">`{ maxCredits?, maxSteps?, maxDurationSeconds? }` per iteration.</ParamField>
<ParamField body="maxTotalCredits" type="integer">Cap across every iteration; the goal parks when it is met.</ParamField>
<ParamField body="initialState" type="object">Seeds every iteration.</ParamField>
<ParamField body="input" type="any">The run input on every iteration, at most 64 KB.</ParamField>
<ParamField body="workflowVersionId" type="string">Pin a version.</ParamField>
<ParamField body="idempotencyKey" type="string">At most 128 characters; a replay answers `200` with the existing goal.</ParamField>

**Response**

`201` with the goal. `400 VALIDATION_FAILED` (`goal-judge-schema-missing-done`, `goal-predicate-invalid`, `goal-cadence-invalid`, `goal-evaluation-invalid`, `goal-objective-too-long`, `cadence-invalid`), `GOAL_MAX_RUNS_INVALID`, or `WORKFLOW_NOT_ON_AGENT`; `409 GOAL_CAP` (`cap`) past the organization's active-goal cap, 20 by default; `503 CONTROL_UNAVAILABLE`.

Equivalent: `lua workflows goals create <workflow> --objective "Reach 50 signups" --judge-predicate 'output.signups gte 50'`, `Workflows.setGoal()`.

<CodeGroup>
  ```bash CLI theme={null}
  lua workflows goals create lead-outreach --objective "Reach 50 trial signups" \
    --judge-predicate 'output.signups gte 50' --cadence '{"type":"interval","seconds":1800}' --max-runs 24
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/goals', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({
      workflowId: '<<WORKFLOW_ID>>',
      objective: 'Reach 50 trial signups',
      judge: { predicate: { path: 'output.signups', op: 'gte', value: 50 } },
      cadence: [{ type: 'interval', seconds: 1800 }],
      maxRuns: 24,
    }),
  });
  const goal: { goalId: string; status: string } = await response.json();
  console.log(goal.goalId, goal.status);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/workflows/<<YOUR_AGENT_ID>>/goals" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{
      "workflowId": "<<WORKFLOW_ID>>",
      "objective": "Reach 50 trial signups",
      "judge": { "predicate": { "path": "output.signups", "op": "gte", "value": 50 } },
      "cadence": [{ "type": "interval", "seconds": 1800 }],
      "maxRuns": 24
    }'
  ```
</CodeGroup>

### GET /workflows/:agentId/goals

Lists goals as `{ "items": [...], "nextCursor"? }`.

<ParamField query="status" type="string">`active`, `paused`, `done`, or `closed`.</ParamField>
<ParamField query="workflowId" type="string">Goals of one definition.</ParamField>
<ParamField query="cursor" type="string">Page cursor.</ParamField>
<ParamField query="limit" type="integer">From 1 to 100.</ParamField>

`400 BAD_CURSOR` or `VALIDATION_FAILED`.

Equivalent: `lua workflows goals list <workflow> --status active`, `Workflows.goals.list()`.

### GET /workflows/:agentId/goals/:goalId

Returns the goal with its run history, `runs: [{ runId, status, iteration?, verdict?, creditsUsed, createdAt, purged? }]`. `404 GOAL_NOT_FOUND`.

Equivalent: `lua workflows goals get <goalId>`.

### PATCH /workflows/:agentId/goals/:goalId

Edits an active or paused goal: `objective`, `judge`, `cadence`, `evaluation` (the mode is fixed at create; `delaySeconds: null` clears the delay), `maxRuns`, `maxTotalCredits` (`null` clears), `budget` (`null` clears), `note` (at most 2,000 characters; `null` clears), and `ifMatch` (the `updatedAt` you read; a mismatch is `409 GOAL_VERSION_CONFLICT` with the current `updatedAt`). Answers the goal, with `rearmed: true` when the edit cleared a budget or run-count park. `400 VALIDATION_FAILED` (an empty edit, or the create-time codes) or `GOAL_MAX_RUNS_INVALID`; `404 GOAL_NOT_FOUND`; `409 GOAL_NOT_ACTIVE` on a done or closed goal.

Equivalent: `lua workflows goals edit <goalId> --max-credits 500 --every 30m`.

### POST /workflows/:agentId/goals/:goalId/raise

Raises `maxTotalCredits` or `maxRuns`, increases only, with an optional `note` and `ifMatch`; the same value twice is a `200` no-op, and the raise is recorded in `raises[]`. `400 GOAL_RAISE_BELOW_SPENT` (`field`, `value`, `spent`), `VALIDATION_FAILED` (nothing to raise, or a value below the current cap), or `GOAL_MAX_RUNS_INVALID`; `404 GOAL_NOT_FOUND`; `409 GOAL_NOT_ACTIVE` or `GOAL_VERSION_CONFLICT`.

Equivalent: `lua workflows goals raise <goalId> --max-runs 24`.

### POST /workflows/:agentId/goals/:goalId/pause

Pauses an active goal; `409 GOAL_NOT_ACTIVE` otherwise. `POST .../resume` resumes a paused goal without backfill, and refuses with `409 GOAL_BUDGET_EXHAUSTED` (`spent`, `cap`) while a credit cap is met. `POST .../close` closes it for good with an optional `note` of at most 2,000 characters. Each answers the goal; `404 GOAL_NOT_FOUND`.

Equivalent: `lua workflows goals pause <goalId>`, `Workflows.goals.pause()`, `resume()`, `close()`.

## Workspaces and Job-tier steps

### GET /workflows/:agentId/runs/:runId/workspace

Returns the run's [workspace](/build/workflows/job-tier) view, `{ runId, kind: "git" | "empty", backend, status, repo?, ref?, credentialsRef?, sizeGb, ttlHours, keepArtefacts?, branch?, baseSha?, headSha?, pushed?, bytesUsed?, filesChanged?, ... }`; `credentialsRef` is an id, never a token. `404 RUN_NOT_FOUND` or `WORKSPACE_NOT_FOUND`.

Equivalent: `lua workflows workspace <runId>`.

### POST /workflows/:agentId/runs/:runId/workspace/release

Releases the workspace early, with an optional `note` (at most 512 characters). Answers `{ "released": true, "status": "released" | "held" | "expired" }` or `{ "released": false, "reason": "not_provisioned" | "already_released" | "held_empty" }`; `404 RUN_NOT_FOUND`; `409 WORKSPACE_IN_USE` with the `stepId` still using it; `503 CONTROL_UNAVAILABLE`.

Equivalent: `lua workflows workspace <runId> --release`.

### GET /workflows/:agentId/runs/:runId/steps/:stepId/job

Returns a Job-tier step's view: `segment` of `segmentsTotalMax`, `checkpoint`, pod phase and timings, and a bounded log tail.

<ParamField query="attempt" type="integer">One attempt instead of the current one.</ParamField>
<ParamField query="tail" type="integer" default="200">Log lines, from 1 to 2,000 (and at most 64 KB).</ParamField>

`400 VALIDATION_FAILED` for a non-numeric `attempt` or `tail`; `404 RUN_NOT_FOUND`, `JOB_NOT_FOUND`, or `JOB_LOGS_NOT_PERSISTED` for a finished attempt that kept nothing; `503 CONTROL_UNAVAILABLE` only for a live attempt.

Equivalent: `lua workflows jobs <runId>`, `lua workflows job-logs <runId> <stepId> --tail 500`.

## Organization-wide routes

### GET /workflows/runs

Lists runs across an organization; `workflows:read` is checked on the organization.

<ParamField query="orgId" type="string" required>The organization. Missing, blank, repeated, or over-long is `400 VALIDATION_FAILED`, never `403`.</ParamField>
<ParamField query="agentId" type="string">Narrow to one agent.</ParamField>

Every filter of `GET /workflows/:agentId/runs` applies. `403` when the caller holds no `workflows:read` on that organization; an unknown organization and another organization look the same. The CLI lists runs per agent only, so there is no CLI tab.

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ orgId: '<<YOUR_ORG_ID>>', status: 'suspended', limit: '100' });
  const response = await fetch(`https://api.heylua.ai/workflows/runs?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { items: Array<{ runId: string; agentId: string; status: string }>; nextCursor: string | null } =
    await response.json();
  console.log(page.items.length);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/workflows/runs" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'orgId=<<YOUR_ORG_ID>>' --data-urlencode 'status=suspended' --data-urlencode 'limit=100'
  ```
</CodeGroup>

### GET /workflows/runs/:runId

Returns a run by id alone, for deep links from inbox receipts and notifications; the scope is checked on the run's agent. Same body as `GET /workflows/:agentId/runs/:runId`. `404 RUN_NOT_FOUND`.

Moving one user's schedules, approvals, connections, and drafts to another user is `POST /workflows/reassign` on [Workflow approvals](/reference/rest/workflow-approvals#reassignment).

## See also

* [Workflows](/reference/rest/workflows) — the run model, run routes, and every error code
* [Goals and schedules](/build/workflows/goals-and-schedules) — when a goal beats a schedule
* [Job-tier steps](/build/workflows/job-tier) — what a workspace is and how a Job-tier step uses it
* [`lua workflows`](/reference/cli/workflows) — `schedules`, `goals`, `workspace`, `jobs`, and `job-logs`
* [`Workflows`](/reference/sdk/workflows) — `setGoal()` and `goals.*` from agent code
