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

# Running a workflow on a schedule

> Start a Flowker workflow on a cadence, read the occurrences it plans to run, and decide what happens to an occurrence that could not run on time.

A schedule trigger starts a workflow on a cadence you write as a cron expression. Flowker records every firing as an occurrence, so a firing that could not happen on time is not lost: it waits in a parked list until you run it or discard it.

Use this page to write the trigger, confirm the cadence, and work the parked list.

## Before you start

***

* A workflow in `draft` status. Only a draft workflow accepts an edit, so write the trigger before you activate it. See [Getting started with Flowker](/en/reference/flowker/flowker-api-quick-start) for the create-and-activate path.
* The worker binary running with the scheduler enabled. `SCHEDULER_ENABLED` resolves to `true` unless you set it to `false`, and the queue needs `SCHEDULER_REDIS_HOST`: with no host the worker binary does not start and no scheduled workflow fires. Check that variable first when your schedules never fire. See [Scheduler variables](/en/flowker/flowker-environment-variables#scheduler).
* The `read` permission on the `workflows` resource to list occurrences and counts, and `update` on the same resource to run or discard one.

## Step 1: Write the schedule trigger node

***

Triggers are built in. You discover them in the catalog, and you never create one. [Get a catalog trigger](/en/reference/flowker/get-catalog-trigger) returns the schedule trigger's JSON Schema from the running instance:

```bash theme={null}
curl -s http://localhost:4021/v1/catalog/triggers/schedule | jq -r '.schema' | jq .
```

The schedule trigger is a node with `type: "trigger"` and these fields in its `data`:

| Field         | When you set it | Value                                                                                                        |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------ |
| `triggerType` | Always          | `"schedule"`.                                                                                                |
| `cron`        | Always          | The cadence, as a standard 5-field cron expression: `minute hour day-of-month month day-of-week`.            |
| `timezone`    | Optional        | The IANA identifier of the zone the cron fields belong to, such as `"America/Sao_Paulo"`. Defaults to `UTC`. |
| `enabled`     | Optional        | `false` stops the schedule from firing while the workflow stays active. Defaults to `true`.                  |

### What the cron expression accepts

Five fields, separated by spaces. Each field takes `*`, a value, a list (`0,30`), a range (`9-17`), or a step (`*/15`, `9-17/2`). Day-of-week runs `0`–`7`, where both `0` and `7` mean Sunday. When you restrict day-of-month and day-of-week at the same time, the schedule fires on a day that matches either field — `0 9 13 * 5` fires on the 13th and on every Friday.

One minute is the finest cadence a 5-field expression can express. For anything faster, take the call in as it arrives with a [webhook trigger](/en/flowker/configuring-a-webhook-trigger).

Flowker checks the expression when you save the workflow and again when you activate it, and answers `FLK-0117` when it does not hold. These forms do not hold:

* A six-field expression, such as `*/30 * * * * *`.
* A macro, such as `@daily` or `@every 5m`.
* A named day or month, such as `MON`, `MON-FRI` or `sun`.
* An `L` or `#` token, such as `0 9 L * *` or `0 9 * * 5#2`.
* A value outside its field's range, such as `60` minutes, `24` hours, day-of-month `0` or `32`, month `13`, or day-of-week `8`.
* A `/0` step.

### How the timezone works

The cron fields are wall-clock time in the zone you name, and every time the API returns is UTC. A `0 9 * * *` schedule in `America/Sao_Paulo` reports `12:00Z`. A zone that observes daylight saving keeps the wall-clock hour across the change: the same expression in `America/New_York` reports `14:00Z` in winter and `13:00Z` in summer.

<CodeGroup>
  ```json daily theme={null}
  {
    "id": "trigger-1",
    "type": "trigger",
    "name": "Daily settlement",
    "position": { "x": 0, "y": 0 },
    "data": {
      "triggerType": "schedule",
      "cron": "0 9 * * *",
      "timezone": "America/Sao_Paulo"
    }
  }
  ```

  ```json every 15 minutes theme={null}
  {
    "id": "trigger-1",
    "type": "trigger",
    "name": "Poll for new statements",
    "position": { "x": 0, "y": 0 },
    "data": {
      "triggerType": "schedule",
      "cron": "*/15 * * * *"
    }
  }
  ```

  ```json weekdays, paused theme={null}
  {
    "id": "trigger-1",
    "type": "trigger",
    "name": "Weekday reconciliation",
    "position": { "x": 0, "y": 0 },
    "data": {
      "triggerType": "schedule",
      "cron": "30 7 * * 1-5",
      "timezone": "America/Sao_Paulo",
      "enabled": false
    }
  }
  ```

  ```json monthly theme={null}
  {
    "id": "trigger-1",
    "type": "trigger",
    "name": "Month-open batch",
    "position": { "x": 0, "y": 0 },
    "data": {
      "triggerType": "schedule",
      "cron": "0 3 1 * *",
      "timezone": "UTC"
    }
  }
  ```
</CodeGroup>

## Step 2: Activate the workflow and read the cadence

***

<Steps>
  <Step title="Save the workflow">
    Send the node with the rest of your workflow to [Create a workflow](/en/reference/flowker/create-workflow), or to [Update a workflow](/en/reference/flowker/update-workflow) if the draft already exists. Flowker validates the cron here.
  </Step>

  <Step title="Check the cadence before you commit to it">
    [List upcoming scheduled occurrences](/en/reference/flowker/list-upcoming-scheduled-occurrences) computes the next firings straight from the trigger, so you can read them while the workflow is still a draft.

    ```bash theme={null}
    curl -s "http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/upcoming?limit=3" | jq .
    ```

    ```json theme={null}
    {
      "occurrences": [
        { "scheduledFor": "2026-08-01T12:00:00Z" },
        { "scheduledFor": "2026-08-02T12:00:00Z" },
        { "scheduledFor": "2026-08-03T12:00:00Z" }
      ]
    }
    ```

    `limit` takes `1` to `50` and defaults to `10`. A value outside that range answers `FLK-0304`.
  </Step>

  <Step title="Activate it">
    Call [Activate a workflow](/en/reference/flowker/activate-workflow). Within a minute the engine records the next occurrence and queues it for its slot.

    ```bash theme={null}
    curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/activate | jq .
    ```
  </Step>
</Steps>

Flowker records only the next firing, never a calendar of future firings. When that occurrence runs, the engine records the firing after it, so the cadence carries itself forward one occurrence at a time.

## Step 3: See which occurrences did not run

***

An occurrence **parks** when the engine reaches it more than a minute after its slot and nobody has asked for that slot to run: the service was down, the queue was behind, the process restarted. Flowker never runs a parked occurrence on its own — it holds it for your decision, in the `pending-review` state. Parking is the one outcome that is not terminal: a parked occurrence stays actionable until you run it or discard it.

Flowker never back-fills a slot that passed. So the parked list holds the firings Flowker had already recorded and could not run — not one entry for every slot that went by during an outage — and the cadence itself resumes from the next future slot.

An occurrence is **skipped** when the engine took it up and closed it without running the workflow. A skipped occurrence is terminal and carries a `skipReason`:

| `skipReason`          | What happened                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `active-run`          | Another run of this workflow was still going. A schedule runs one occurrence at a time, so this firing stood down. |
| `workflow-gone`       | The workflow was deleted, or it was not active when the occurrence reached the engine.                             |
| `execution-duplicate` | The work for that slot had already run, so the engine did not run it twice.                                        |

The two classes do not split on timing. One thing timing does decide: a late slot you never asked to run lands in the parked list, never in the skipped list. After you ask for a parked slot to run, its age stops holding it back. The engine then takes it up like any other occurrence, and all three reasons above can close it. So a `scheduledFor` far in the past is normal in the skipped list, and it does not mean the slot fired on time. [Step 4](#step-4-run-or-discard-a-parked-occurrence) covers what your own run can end in.

Three reads cover the whole picture:

<Steps>
  <Step title="List the parked occurrences of one workflow">
    [List parked scheduled occurrences](/en/reference/flowker/list-parked-scheduled-occurrences) returns them oldest first.

    ```bash theme={null}
    curl -s http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed | jq .
    ```

    ```json theme={null}
    {
      "occurrences": [
        {
          "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30",
          "workflowId": "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a",
          "status": "pending-review",
          "cronExpr": "0 9 * * *",
          "timezone": "America/Sao_Paulo",
          "scheduledFor": "2026-07-29T12:00:00Z",
          "attempts": 0,
          "createdAt": "2026-07-29T11:00:04Z",
          "updatedAt": "2026-07-30T08:12:41Z"
        }
      ]
    }
    ```

    `scheduledFor` is the slot the occurrence stands for, and `status` is what decides whether you can act on it.

    This route takes no `limit` and no page cursor, and it returns at most 100 occurrences. Plan a bulk recovery around that: work the rows you get, then read the list again. The count below reports the real total. Discarding the whole list also covers every occurrence pending review, not only the 100 a single read shows you.
  </Step>

  <Step title="List the skipped occurrences">
    [List skipped scheduled occurrences](/en/reference/flowker/list-skipped-scheduled-occurrences) returns them oldest first, each with its `skipReason`. `limit` takes `1` to `50`.

    ```bash theme={null}
    curl -s "http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/skipped?limit=10" | jq '.occurrences[] | {scheduledFor, skipReason}'
    ```

    ```json theme={null}
    { "scheduledFor": "2026-07-28T12:00:00Z", "skipReason": "active-run" }
    ```

    Repeated `active-run` skips mean the workflow takes longer than the gap between two firings. Widen the cadence, or make the workflow finish faster.
  </Step>

  <Step title="Count what is waiting across every workflow">
    [Count pending-review occurrences](/en/reference/flowker/count-pending-review-occurrences) answers for the whole tenant in one call, which is what you poll for a review badge. The map is sparse: a workflow with nothing waiting is absent from it.

    ```bash theme={null}
    curl -s http://localhost:4021/v1/workflows/schedule/pending-review-counts | jq .
    ```

    ```json theme={null}
    {
      "counts": {
        "019c96a0-0ac0-7de9-9f53-9cf842a2ee5a": 2
      }
    }
    ```

    Add `?workflowIds=<id>,<id>` to scope the counts to the workflows you care about.
  </Step>
</Steps>

Both lists answer `200` with an empty `occurrences` array for a workflow id your tenant does not own, so an empty list means "nothing to review here".

<Tip>
  The Console shows the same three reads. Open the **Schedule** panel from the workflow list to see the schedule status, the upcoming runs, and the runs pending review. See [Workflows overview](/en/flowker/console/workflows-overview).
</Tip>

## Step 4: Run or discard a parked occurrence

***

Run and discard act on an occurrence whose `status` is `pending-review`. Any other state answers `FLK-0755`, which also makes a repeated call safe: the second one is rejected instead of acting twice. Your own run puts the occurrence in one of those other states: it leaves `pending-review` straight away.

A run you ask for leaves the parked list at once, and the age of the slot no longer holds it back. It does not promise that the workflow runs:

* **The workflow runs.** The execution appears in [List executions](/en/reference/flowker/list-executions) for that workflow.
* **The engine closes the occurrence as skipped.** It leaves the parked list for the skipped list with one of the three reasons above. `active-run` means another run of the workflow was still going. `workflow-gone` means the workflow was not active when your run reached the engine. `execution-duplicate` means the work for that slot had already run.

A skip from your own run is as terminal as any other, so a second run or a discard of it answers `FLK-0755`. Read both lists before you conclude anything about a slot you tried to recover. The skipped row may record the refusal of your recovery, not of the original firing.

Keep the workflow active while you work the list. A run reloads the workflow, and a workflow that is not active closes the occurrence as a `workflow-gone` skip instead of running it.

<Steps>
  <Step title="Run one occurrence">
    [Run a parked occurrence](/en/reference/flowker/run-parked-occurrence) moves it to `queued` and hands it to the same execution path a scheduled firing uses. It starts within a second or two.

    ```bash theme={null}
    curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/run | jq '{id, status}'
    ```

    ```json theme={null}
    { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "status": "queued" }
    ```

    The run covers that one slot. It does not shift the cadence: the next firing stays the one the engine already planned.
  </Step>

  <Step title="Discard one occurrence">
    [Discard a parked occurrence](/en/reference/flowker/discard-parked-occurrence) moves it to `discarded`, which is terminal. The occurrence never executes and leaves the parked list.

    ```bash theme={null}
    curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30/discard | jq '{id, status}'
    ```

    ```json theme={null}
    { "id": "019c96a0-3f21-7b44-8d0e-5a1c7e2b9f30", "status": "discarded" }
    ```
  </Step>

  <Step title="Clear the whole parked list of one workflow">
    [Discard all parked occurrences](/en/reference/flowker/discard-all-parked-occurrences) discards, in a single write, every occurrence of that workflow that is pending review, and reports how many it moved.

    ```bash theme={null}
    curl -s -X POST http://localhost:4021/v1/workflows/019c96a0-0ac0-7de9-9f53-9cf842a2ee5a/schedule/missed/discard-all | jq .
    ```

    ```json theme={null}
    { "discarded": 7 }
    ```

    With nothing pending review it answers `200` with `"discarded": 0`, so a repeat call is safe.
  </Step>
</Steps>

<Warning>
  A discard cannot be undone, and it never runs anything. Read the parked list before you clear it.

  Discarding all parked occurrences covers exactly the occurrences of that one workflow, in your tenant, that are pending review. It leaves the schedule itself running, leaves the upcoming occurrences alone, and does not touch another workflow's occurrences, or occurrences that already ran, failed, were skipped, or were discarded earlier.
</Warning>

## Confirm it worked

***

* The cadence is healthy when [List upcoming scheduled occurrences](/en/reference/flowker/list-upcoming-scheduled-occurrences) returns future slots and the parked list stays short.
* A run worked when the occurrence has left the parked list and the execution shows up in [List executions](/en/reference/flowker/list-executions) for that workflow.
* A discard worked when the occurrence has left the parked list and the pending-review count for that workflow has dropped.

## Change or pause a cadence

***

Only a draft workflow accepts an edit, so a cadence change is four calls:

1. [Deactivate the workflow](/en/reference/flowker/deactivate-workflow). Flowker stops recording new occurrences for it.
2. [Move it to draft](/en/reference/flowker/move-workflow-to-draft).
3. [Update the workflow](/en/reference/flowker/update-workflow) with the new `cron`, `timezone`, or `enabled` value.
4. [Activate it](/en/reference/flowker/activate-workflow). Within a minute the engine records the next occurrence from the new cadence.

Flowker does not back-fill the slots that passed while the workflow was inactive, and the parked occurrences survive all four steps: they stay listed and stay actionable once the workflow is active again.

<Note>
  An occurrence Flowker recorded before the change is still owed. Read the parked list after a cadence change, and clear anything you no longer want.
</Note>

## When something goes wrong

***

| Code       | When it happens                           | What to do                                                                                                                                                                                             |
| ---------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `FLK-0117` | You save or activate the workflow.        | The cron is not a standard 5-field expression. Compare it against [What the cron expression accepts](#what-the-cron-expression-accepts).                                                               |
| `FLK-0118` | You list the upcoming occurrences.        | The workflow carries no schedule trigger, or its cron is not an expression Flowker can compute. Check the trigger node's `triggerType` and `cron`.                                                     |
| `FLK-0100` | You list the upcoming occurrences.        | No workflow in your tenant has that id.                                                                                                                                                                |
| `FLK-0002` | Any of these calls.                       | A path id is not a valid UUID.                                                                                                                                                                         |
| `FLK-0304` | You list upcoming or skipped occurrences. | `limit` is outside `1`–`50`.                                                                                                                                                                           |
| `FLK-0755` | You run or discard an occurrence.         | The occurrence is not pending review. Your own run may have moved it on already, or it may have run, been skipped, or been discarded. Read the parked list and the skipped list before you call again. |
| `FLK-0760` | You run or discard an occurrence.         | No occurrence of that workflow has that id. Take the id from the parked list of the same workflow.                                                                                                     |

Two failures answer no error code:

* **Upcoming occurrences are listed, but nothing ever runs.** The API computes the cadence on its own, while the worker binary is what fires it. Confirm the worker is running and that `SCHEDULER_REDIS_HOST` is set. See [Scheduler variables](/en/flowker/flowker-environment-variables#scheduler).
* **Nothing new is recorded for an active workflow.** Check `enabled` on the trigger node: `false` keeps the workflow active and its schedule quiet.

## What's next

***

<CardGroup cols={2}>
  <Card title="Configuring a webhook trigger" icon="webhook" href="/en/flowker/configuring-a-webhook-trigger">
    Start the same workflow from an inbound HTTP call instead of a cadence.
  </Card>

  <Card title="Workflow design guide" icon="diagram-project" href="/en/flowker/workflow-design-guide">
    Build the rest of the graph the trigger enters.
  </Card>
</CardGroup>
