# Workflow Scheduler — Quickstart

This guide walks through the full lifecycle of a scheduled workflow using `curl`.
All examples assume Conductor is running locally on port 8080.

---

## Prerequisites

- Conductor running with a scheduler-compatible persistence backend
  (`conductor-scheduler-postgres-persistence`, `conductor-scheduler-mysql-persistence`, etc.)
- `conductor.scheduler.enabled=true` (default)
- The `http-task` worker available (built-in for HTTP tasks; swap for `SIMPLE` if needed)

---

## Step 1 — Register the workflow

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" \
  -d @daily-report-workflow.json
```

---

## Step 2 — Create a schedule

`every-minute-schedule.json` fires every minute — useful for seeing results quickly.
Swap in `daily-report-schedule.json` for a realistic weekday 9 AM (New York) schedule.

```bash
curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" \
  -d @every-minute-schedule.json | jq .
```

Expected response:
```json
{
  "name": "every-minute-demo-schedule",
  "cronExpression": "0 * * * * *",
  "zoneId": "UTC",
  "paused": false,
  "nextRunTime": 1708300860000
}
```

---

## Step 3 — Preview next execution times

```bash
curl -s "http://localhost:8080/api/scheduler/nextFewSchedules?cronExpression=0+*+*+*+*+*&limit=5" \
  | jq '[.[] | (. / 1000 | todate)]'
```

---

## Step 4 — Check execution history

After a minute or two, executions will appear:

```bash
curl -s "http://localhost:8080/api/scheduler/search/executions?freeText=every-minute-demo-schedule&size=5" \
  | jq '.results[] | {state, workflowId, scheduledTime}'
```

Expected output:
```json
{ "state": "EXECUTED", "workflowId": "abc123...", "scheduledTime": 1708300860000 }
{ "state": "EXECUTED", "workflowId": "def456...", "scheduledTime": 1708300800000 }
```

---

## Step 5 — Pause the schedule

```bash
curl -s "http://localhost:8080/api/scheduler/schedules/every-minute-demo-schedule/pause?reason=testing+pause"
```

Verify it's paused:
```bash
curl -s http://localhost:8080/api/scheduler/schedules/every-minute-demo-schedule | jq '{paused, pausedReason}'
```

---

## Step 6 — Resume the schedule

```bash
curl -s http://localhost:8080/api/scheduler/schedules/every-minute-demo-schedule/resume
```

---

## Step 7 — List all schedules

```bash
curl -s http://localhost:8080/api/scheduler/schedules | jq '[.[] | {name, cronExpression, paused, nextRunTime}]'
```

Filter by workflow name:
```bash
curl -s "http://localhost:8080/api/scheduler/schedules?workflowName=daily_report_workflow" | jq .
```

---

## Step 8 — Delete the schedule

```bash
curl -s -X DELETE http://localhost:8080/api/scheduler/schedules/every-minute-demo-schedule
```

---

## API Reference

| Method   | Path                                                   | Description                                    |
|----------|--------------------------------------------------------|------------------------------------------------|
| `POST`   | `/api/scheduler/schedules`                             | Create or update a schedule                    |
| `GET`    | `/api/scheduler/schedules`                             | List all (optional `?workflowName=` filter)    |
| `GET`    | `/api/scheduler/schedules/search`                      | Search schedules (filter by name, workflow, paused) |
| `GET`    | `/api/scheduler/schedules/{name}`                      | Get a schedule by name                         |
| `DELETE` | `/api/scheduler/schedules/{name}`                      | Delete a schedule                              |
| `GET`    | `/api/scheduler/schedules/{name}/pause`                | Pause (optional `?reason=`)                    |
| `GET`    | `/api/scheduler/schedules/{name}/resume`               | Resume                                         |
| `GET`    | `/api/scheduler/nextFewSchedules`                      | Preview next N times (`?cronExpression=&limit=5`) |
| `GET`    | `/api/scheduler/search/executions`                     | Search execution history (`?freeText=&size=100`) |

---

## Cron expression format

The scheduler uses **6-field Spring cron** (second-level precision):

```
┌─────────────── second (0-59)
│ ┌───────────── minute (0-59)
│ │ ┌─────────── hour (0-23)
│ │ │ ┌───────── day of month (1-31)
│ │ │ │ ┌─────── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌───── day of week (0-7 or MON-SUN)
│ │ │ │ │ │
* * * * * *
```

| Expression                    | Meaning                          |
|-------------------------------|----------------------------------|
| `0 * * * * *`                 | Every minute                     |
| `0 0 9 * * MON-FRI`           | Weekdays at 9:00 AM              |
| `0 0 0 1 * *`                 | First day of every month         |
| `0 0/30 9-17 * * MON-FRI`     | Every 30 min, business hours     |

---

## Configuration

```yaml
conductor:
  scheduler:
    enabled: true                      # default: true
    polling-interval: 1000             # ms between polls; default: 100
    polling-thread-count: 1            # default: 1
    poll-batch-size: 5                 # schedules processed per cycle; default: 5
    scheduler-time-zone: UTC           # default: UTC
    archival-max-records: 5            # history rows to keep per schedule; default: 5
    archival-max-record-threshold: 10  # prune when over threshold; default: 10
    jitter-max-ms: 0                   # dispatch jitter per schedule; default: 0 (disabled)
```

> **Tip:** For deployments with many schedules firing at the same cron tick, increase
> `poll-batch-size` to match expected fanout and set `jitter-max-ms` to a small value
> (e.g. 200) to smooth burst load on the DB and executor pool.

---

## Example Scenarios

Eight verified scenarios covering common patterns. Each has a workflow definition and a
matching schedule definition. All were tested live.

### 1. Basic (`every-minute-schedule.json` + `daily-report-workflow.json`)

Fires every minute, fetches a sample JSON dataset via HTTP. Good first test after setup.

**Register and run:**
```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @daily-report-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @every-minute-schedule.json
```

---

### 2. Catchup mode (`catchup-schedule.json` + `catchup-workflow.json`)

Sets `runCatchupScheduleInstances: true`. If the scheduler is offline for N minutes, it fires
once per missed slot on restart — stepping slot-by-slot rather than jumping to the current time.

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @catchup-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @catchup-schedule.json
```

To observe catchup: stop Conductor for a few minutes, then restart and watch executions fire
in sequence for the missed slots.

---

### 3. Bounded schedule (`bounded-schedule-template.json` + `bounded-workflow.json`)

Uses `scheduleStartTime` / `scheduleEndTime` (epoch ms) to confine execution to a window.
The template has `__START_MS__` / `__END_MS__` placeholders — populate with `sed`:

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @bounded-workflow.json

NOW=$(python3 -c "import time; print(int(time.time()*1000))")
END=$((NOW + 300000))  # 5-minute window
sed "s/__START_MS__/$NOW/; s/__END_MS__/$END/" bounded-schedule-template.json | \
  curl -s -X POST http://localhost:8080/api/scheduler/schedules \
    -H "Content-Type: application/json" -d @-
```

---

### 4. Multi-step FORK/JOIN (`multistep-schedule.json` + `multistep-workflow.json`)

Two parallel HTTP calls (UTC time + America/New_York time), joined into one output map.

> **Gotcha:** Use a literal `/` in timezone query params — not `%2F`. Conductor's HTTP task
> passes percent-encoded slashes literally, which the remote API rejects as an invalid timezone.

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @multistep-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @multistep-schedule.json
```

---

### 5. Failure scenario (`retry-schedule.json` + `retry-workflow.json`)

Workflow always fails (404). Confirms the scheduler fires every minute regardless of prior
outcome — each tick produces a new `EXECUTED` record in scheduler history even as the workflow
itself records `FAILED`.

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @retry-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @retry-schedule.json
```

---

### 6. Concurrent execution (`concurrent-schedule.json` + `concurrent-workflow.json`)

A 90-second WAIT task fired every 60 seconds. OSS Conductor has no built-in concurrent-execution
guard, so instances stack up. Demonstrates the behavior users need to design around.

> **Gotcha:** WAIT task duration must be `"90s"` / `"2m"` / `"1h"` — not ISO-8601 `PT90S`.
> Conductor's `DateTimeUtils.parseDuration` uses its own regex, not the Java Duration parser.

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @concurrent-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @concurrent-schedule.json
```

---

### 7. Input parameterization (`input-param-schedule.json` + `input-param-workflow.json`)

Every triggered workflow automatically receives `_scheduledTime` and `_executedTime` (epoch ms)
injected by the scheduler. Static keys from `startWorkflowRequest.input` are preserved.
An INLINE JavaScript task computes a 24-hour report window from `scheduledTime`.

Sample output from a live run:
```
scheduledAt:       2026-02-19T23:22:00.000Z   ← exact cron slot
triggeredAt:       2026-02-19T23:22:00.837Z   ← actual dispatch (~837ms poll overhead)
reportWindowStart: 2026-02-18T23:22:00.000Z
reportWindowEnd:   2026-02-19T23:22:00.000Z
```

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @input-param-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @input-param-schedule.json
```

---

### 8. DO_WHILE variant (`dowhile-schedule.json` + `dowhile-workflow.json`)

Internally loops 3 times via DO_WHILE, fetching current time on each iteration.

> **Gotcha:** DO_WHILE output is keyed by iteration number as a string (`"1"`, `"2"`, `"3"`),
> not by task reference name. Reference the last iteration's output via:
> `${poll_loop.output.3.fetch_current_time.response.body.dateTime}`

```bash
curl -s -X POST http://localhost:8080/api/metadata/workflow \
  -H "Content-Type: application/json" -d @dowhile-workflow.json

curl -s -X POST http://localhost:8080/api/scheduler/schedules \
  -H "Content-Type: application/json" -d @dowhile-schedule.json
```

---

## Concurrency / Load Test Scripts

The `../scripts/` directory contains four scripts from live concurrency testing.
All require `curl`, `python3`, and a running Conductor instance.

### test-09-concurrent-write.sh — simultaneous schedule registration

Run on two machines at the same epoch second to verify UPSERT correctness:
```bash
# Both machines run this pointing at the same Conductor instance
./scripts/test-09-concurrent-write.sh http://localhost:8080
```

### test-10-concurrent-resume.sh — simultaneous resume

Verifies that a paused schedule resumed from two machines fires exactly once:
```bash
# Machine 1 (setup + fire)
./scripts/test-10-concurrent-resume.sh setup http://localhost:8080
# Follow the printed instructions to run the fire command on both machines simultaneously
```

### test-11-thundering-herd.sh — N schedules at the same tick

Registers N schedules all firing at `0 * * * * *`, then verifies each fires exactly once:
```bash
./scripts/test-11-thundering-herd.sh 50 http://localhost:8080
```

> **Note:** Requires `poll-batch-size >= N` (or multiple poll cycles). With the default
> `poll-batch-size=5`, only 5 schedules fire per cycle. Increase it before running with N > 5.

### test-12-load-blast.py — concurrent workflow submissions

Blasts N `POST /api/workflow` requests simultaneously, reports latency percentiles:
```bash
# Single machine
python3 scripts/test-12-load-blast.py --url http://localhost:8080 --count 25

# Two machines synchronized to the same epoch second
TARGET=$(python3 -c "import time; print(int(time.time())+15)")
# Machine 1:
python3 scripts/test-12-load-blast.py --url http://localhost:8080 --count 25 --target $TARGET
# Machine 2:
python3 scripts/test-12-load-blast.py --url http://<other-host>:8080 --count 25 --target $TARGET
```
