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

# Admin Control Plane Workflow Monitors

> Internal-only admin contract that lets the Cloudflare control plane load due workflow monitors and execute monitor ticks through the existing shared workflow facade.

These routes are the first concrete migration seam from `apps/engine`
workflow-monitor orchestration into Cloudflare Workflows.

The ownership split is:

* Cloudflare `agent-control-plane` owns due scanning cadence and durable workflow instances
* `platform-api` owns the thin authenticated admin surface
* `WorkflowsFacade.tickMonitor(...)` remains the deterministic shared mutation path

## Scope

* Internal-only
* Protected by `ADMIN_AUTH_SECRET_TOKEN`
* Intended for `apps/agent-control-plane`, not browser traffic

## Endpoints

### `POST /v1/admin/control-plane/workflow-monitors/due`

Load runnable monitor rows from the canonical workflow-monitor repository.

Request body:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:30:00.000Z",
  "limit": 100
}
```

Response:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:30:00.000Z",
  "due": [
    {
      "monitorStatus": "waiting",
      "nextCheckAt": "2026-04-14T12:00:00.000Z",
      "ownerAddress": "0x1111111111111111111111111111111111111111",
      "requestId": "workflow-monitor-tick:wf_due_1:2026-04-14T12:30:00.000Z",
      "workerId": "cloudflare:workflow-monitor:wf_due_1",
      "workflowId": "wf_due_1",
      "workflowStatus": "active"
    }
  ]
}
```

Notes:

* `requestId` is the stable idempotency key Cloudflare should preserve when creating its own workflow instance
* `workerId` is the stable worker identity passed through to `tickMonitor(...)`
* due selection stays authoritative in the shared workflow repository for now

### `POST /v1/admin/control-plane/workflow-monitors/tick`

Execute one monitor tick through `WorkflowsFacade.tickMonitor(...)`.

Request body:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:30:00.000Z",
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "requestId": "workflow-monitor-tick:wf_due_1:2026-04-14T12:30:00.000Z",
  "triggerEvent": {
    "kind": "market_price",
    "watchKey": "market.price:eth:8453:coinbase",
    "summary": "ETH crossed the configured threshold.",
    "dependencies": {
      "marketPrices": [
        {
          "assetId": "eth:8453",
          "usd": 1250,
          "venue": "coinbase"
        }
      ]
    }
  },
  "workerId": "cloudflare:workflow-monitor:wf_due_1",
  "workflowId": "wf_due_1"
}
```

`triggerEvent` is optional. The current accepted kinds are:

* `market_price`
* `market_candles`
* `approval_pending_count`
* `account_buying_power_usd`
* `account_cash_usd`
* `portfolio_nav_usd`
* `portfolio_cash_pct`
* `portfolio_drift_from_target`
* `portfolio_concentration`
* `portfolio_held_asset_performance`
* `portfolio_held_asset_metrics`

Response:

```json theme={null}
{
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "requestId": "workflow-monitor-tick:wf_due_1:2026-04-14T12:30:00.000Z",
  "userId": "usr_123",
  "workerId": "cloudflare:workflow-monitor:wf_due_1",
  "workflow": {
    "workflowId": "wf_due_1",
    "title": "Monitor ETH",
    "status": "pending_approval",
    "kind": "monitor_then_action",
    "triggerKind": "authored_sdk",
    "linkedPortfolioIds": ["pf_123"],
    "linkedOperationIds": [],
    "nextRunAt": null,
    "lastRunAt": null
  },
  "currentExecution": null,
  "monitorState": {
    "status": "ready",
    "triggerSummary": "ETH crossed the configured threshold.",
    "readyAt": "2026-04-14T12:30:00.000Z",
    "startedAt": null,
    "retryAfter": null,
    "waitingUntil": null,
    "completedAt": null,
    "failedAt": null
  }
}
```

Notes:

* `userId` is resolved from the canonical linked wallet address when available
* Cloudflare uses `userId` plus `linkedPortfolioIds[0]` to mirror the monitor tick into `UserAgent` / `PortfolioAgent`
* this route is intentionally thin; it does not invent a new monitor execution model

### `POST /v1/admin/control-plane/workflow-monitors/notify-execution`

Validate a terminal execution ref and forward it into the Cloudflare-owned
`enqueue-tick` seam.

Request body:

```json theme={null}
{
  "asOfIso": "2026-04-14T18:10:00.000Z",
  "executionRequestId": "req_wf_due_1_step_1",
  "operationId": "op_wf_due_1_step_1",
  "runId": "run_wf_due_1_step_1"
}
```

Response:

```json theme={null}
{
  "asOfIso": "2026-04-14T18:10:00.000Z",
  "enqueued": true,
  "matchedExecution": {
    "operationId": "op_wf_due_1_step_1",
    "runId": "run_wf_due_1_step_1"
  },
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "portfolioId": "pf_123",
  "requestId": "workflow-monitor-execution:wf_due_1:2026-04-14T18:10:00.000Z",
  "workerId": "cloudflare:workflow-monitor-execution:wf_due_1",
  "workflowId": "wf_due_1"
}
```

Notes:

* this route is the event-driven companion to `due` plus `tick`
* it can resolve the owning workflow from canonical `workflow_execution_refs`
  using `operationId`, `runId`, `executionRequestId`, and `eventId`
* it also resolves the primary linked `portfolioId` so the Cloudflare worker
  can look up an already waiting monitor session on `PortfolioAgent`
* if no execution ref matches, it returns `202` with `enqueued: false` instead
  of treating the callback as an error
* it forwards into `POST /api/workflow-monitors/enqueue-tick` on the
  Cloudflare worker instead of mutating workflow state locally

### `POST /v1/admin/control-plane/workflow-monitors/notify-trigger-match`

Forward a centralized trigger-match event directly into the Cloudflare-owned
monitor workflow lane without going through `due`.

Request body:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:31:00.000Z",
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "triggerSummary": "ETH crossed the configured threshold.",
  "workflowId": "wf_due_1"
}
```

Response:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:31:00.000Z",
  "enqueued": true,
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "portfolioId": "pf_123",
  "requestId": "workflow-monitor-trigger:wf_due_1:2026-04-14T12:31:00.000Z",
  "triggerSummary": "ETH crossed the configured threshold.",
  "workerId": "cloudflare:workflow-monitor-trigger:wf_due_1",
  "workflowId": "wf_due_1"
}
```

Notes:

* this is the first explicit ingress for a future centralized trigger matcher
* `platform-api` still validates the workflow and owner via
  `WorkflowsFacade.get(...)`
* the optional `triggerEvent` payload now supports authoritative fact packets
  for `market_price`, `market_candles`, `approval_pending_count`,
  `account_buying_power_usd`, `account_cash_usd`, `portfolio_nav_usd`,
  `portfolio_cash_pct`, `portfolio_drift_from_target`,
  `portfolio_concentration`, `portfolio_held_asset_performance`, and
  `portfolio_held_asset_metrics`, plus the indexer-v4 native
  `portfolio_held_asset_accounting_metrics` packet for finalized/provisional
  accounting readiness, FIFO basis completeness, PnL, and source-finality
  metadata
* it resolves the primary linked `portfolioId` so Cloudflare can mirror the
  resulting monitor workflow state into `PortfolioAgent` / `UserAgent`
* today it forwards into the same `POST /api/workflow-monitors/enqueue-tick`
  seam using `reason: "trigger_match"`
* this is intentionally narrower than the future global matcher fabric; it is
  just the authenticated trigger-delivery seam

### `POST /v1/admin/control-plane/workflow-monitors/recover`

Replay a stale or orphaned monitor wait through the Cloudflare-owned runtime.

Request body:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:45:00.000Z",
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "portfolioId": "pf_123",
  "workflowId": "wf_due_1"
}
```

Response:

```json theme={null}
{
  "asOfIso": "2026-04-14T12:45:00.000Z",
  "enqueued": true,
  "ownerAddress": "0x1111111111111111111111111111111111111111",
  "portfolioId": "pf_123",
  "requestId": "workflow-monitor-recovery:wf_due_1:2026-04-14T12:45:00.000Z",
  "workerId": "cloudflare:workflow-monitor-recovery:wf_due_1",
  "workflowId": "wf_due_1"
}
```

Notes:

* this is an operator/admin replay surface, not a normal callback path
* Cloudflare recovery is intentionally narrow and safe:
  * replay an active `execution_completed` or `trigger_matched` wait on the
    same workflow instance when possible
  * start a fresh authoritative tick only when no active instance is
    registered
  * fail closed when the active workflow is only sleeping on time instead of
    waiting on an event
* optional `triggerEvent` fact packets can be supplied when recovery should
  replay a matcher-driven wake with authoritative dependency data

## Runtime Behavior

The current flow is:

1. Cloudflare calls `due`
2. Cloudflare creates `WorkflowMonitorTickWorkflow` instances with the returned ids
3. Each Cloudflare workflow calls `tick`
4. `platform-api` delegates to `WorkflowsFacade.tickMonitor(...)`
5. Cloudflare mirrors the resulting state into durable user/portfolio actors

The current event-driven companion flow is:

1. engine persists a terminal execution update with canonical execution refs
2. engine calls `notify-execution`
3. `platform-api` resolves the owning workflow from canonical execution refs
4. `platform-api` forwards into Cloudflare `enqueue-tick` with the linked
   `portfolioId`
5. Cloudflare tries to route the callback directly into the active waiting
   `WorkflowMonitorTickWorkflow` session via `sendEvent("execution_completed")`
6. if no active waiting session is registered, Cloudflare falls back to the
   previous `create -> tick` path
7. the workflow calls `tick`
8. Cloudflare mirrors the resulting state into durable user/portfolio actors

The current trigger-ingress companion flow is:

1. a centralized matcher or other authoritative producer detects a match
2. it calls `notify-trigger-match`
3. `platform-api` validates the workflow and owner
4. `platform-api` forwards into Cloudflare `enqueue-tick` with
   `reason: "trigger_match"`
5. Cloudflare starts the durable `WorkflowMonitorTickWorkflow` without waiting
   for a global due scan
6. the workflow calls `tick`
7. Cloudflare mirrors the resulting state into durable user/portfolio actors

This keeps the durable orchestration move in Cloudflare while preserving the
shared workflow facade as the mutation authority during migration.
