---
title: "Submit an eval-episode job for asynchronous execution"
method: POST
path: "/rl/environments/{environmentId}/eval-episodes"
tags: ["RL Environments"]
---

# Submit an eval-episode job for asynchronous execution

`POST /rl/environments/{environmentId}/eval-episodes`

## Request

- Base URL: `https://www.cloudworldmodel.ai/api`
- URL: `https://www.cloudworldmodel.ai/api/rl/environments/{environmentId}/eval-episodes`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-PAYMENT`
  - API key in header `PAYMENT-SIGNATURE`

Replays one or more ordered action sequences through a fresh episode reset and returns
per-episode cumulative rewards computed with `evalCostOverrides` applied. Use this to
detect **reward blind spots**: policies trained without egress or cross-AZ cost pricing
will show a significant reward drop (reward_collapse) when those dimensions are priced.

The endpoint does **not** mutate any stored environment or simulation state — all
episode rollouts are in-memory and ephemeral.

**Async mode (default):** Returns `202 Accepted` immediately with a `jobId`. Poll
`GET /rl/environments/{environmentId}/eval-episodes/{jobId}` until `status` is
`completed` or `failed`.

**Sync mode (`?sync=true`):** Runs the rollout in-process and returns the full result
inline with `200 OK`. Suitable for small workloads (≤ 3 episodes, ≤ 20 steps each).

**reward_collapse logic:** `reward_collapse` is `true` when
`meanEvalReward < trainingTotalReward − collapseThreshold × |trainingTotalReward|`.
With the default threshold of 0.20, any policy whose eval reward is more than 20 %
below its training reward is flagged as collapsing.

## Path parameters

- `environmentId` string, uuid, required

## Query parameters

- `sync` boolean

## Request body

- object
  - `actions` array[], required: Array of episodes. Each episode is an ordered array of actions to replay from a fresh reset. All episodes use the same `evalCostOverrides`. Maximum 10 episodes.
    - Action[]: Ordered sequence of actions for one eval episode
      - `type` 'adjust_threshold' | 'scale_out' | 'scale_in' | 'add_resource' | 'remove_resource' | 'no_op' | 'set_recovery_policy', required: Type of action to execute
      - `parameters` union, required: Type-specific values for the selected action; pass an empty object when the action has no parameters.
        - AdjustThresholdParams: Parameters for adjust_threshold action
          - `cpuThreshold` number: CPU utilization threshold for scaling (%)
          - `throughputThreshold` number: Throughput utilization threshold (%)
          - `latencyThreshold` number: Latency threshold for scaling (ms)
        - ScaleParams: Parameters for scaling the compute fleet in or out.
          - `instanceCount` integer, required: Number of instances to add (`scale_out`) or remove (`scale_in`) — a delta, not a target count. The effective change is clamped to the per-resource instance bounds: the ceiling is `min(autoscalingConfig.maxInstances, characteristics.maxInstances)` for `scale_out`, and the floor is `max(autoscalingConfig.minInstances, characteristics.minInstances)` for `scale_in`. When clamping occurs the step response `info` object contains the flat keys `scale_clamped: true`, `requested` (this parameter's value), `actual` (delta applied, 0 if blocked), and `limit` (effective bound).
        - ResourceParams: Resource details for adding a resource, or its identifier when removing one.
          - `resource` Resource
            - `metadata` object: Resource metadata; configured Route 53 nodes persist modeled lifecycle telemetry under route53Failover.
              - …
            - `id` string, required: Unique resource identifier
            - `dataState` 'intact' | 'temporarily_unavailable' | 'permanently_lost': Data durability, separate from service health; permanently_lost is irreversible.
            - `recoverySource` 'none' | 'surviving_backup' | 'surviving_replica'
            - `recoverySourceResourceId` string
            - `databaseRebuilt` boolean: Explicitly rebuilt empty database; does not restore original lost data.
            - `type` 'compute' | 'network' | 'database' | 'storage' | 'kubernetes' | 'cache' | 'queue', required: Resource type. `kubernetes` models a managed container node pool (EKS / GKE / AKS / OKE / DOKS): its `characteristics.nodeCount` worker nodes autoscale between `characteristics.minNodes` and `characteristics.maxNodes`, growing both serving capacity (`maxThroughput`) and hourly cost proportionally.
            - `name` string, required: Human-readable resource name
            - `provider` 'aws' | 'gcp' | 'azure' | 'oci' | 'digitalocean', required: Cloud provider
            - `status` 'healthy' | 'warning' | 'critical' | 'offline', required: Current resource status
            - `location` object: Resource location for create and read. Owned typical requires regionKey us-east-2 on all four nodes; the server stores canonical use2. Do not set region under characteristics.
              - …
            - `characteristics` object: Provider-specific resource characteristics. For AWS RDS resources, lifecycle/support and version inputs such as `extendedSupport`, `engineVersion`, `databaseEngine`, `databaseEngineVersion`, and `dbEngineVersion` are rejected with `INVALID_FIELD`: CWM models base resource rates and behavior but does not price RDS lifecycle/support surcharges. Direct `engine` is also rejected except for literal `engine: "mysql"` as workload-only identity on the exact AWS `db.r5.large` database in the canonical owned CRUD graph; prefer `workloadDatabaseEngine: "mysql"`. This alias does not declare engine version, Extended Support, lifecycle, or billing. The bounded SQL Server Express contract instead uses `rdsSqlServerBilling.engine: "sqlserver-ex"` inside its nested, strict configuration. Arbitrary data remains supported under resource-level `metadata`. For Aurora Serverless, set ACU limits with `config.minCapacity` and `config.maxCapacity` (or flat `minCapacity` and `maxCapacity`). For AWS aurora-serverless databases with size `db.serverless`, flat `minAcu` and `maxAcu` are aliases when the corresponding canonical field is absent. Top-level and nested config aliases are rejected. A modeled two-instance pair cannot change its topology or ACU bounds through PATCH. Owned AWS CRUD calibration eligibility is an exact predicate: exactly four healthy, routable AWS resources and exactly four edges, one internal ALB → each of two compute apps → one database, with no extra resources or edges. Both apps are `m5.large`; each absent `serviceFamily` is inferred as `ec2` for fit eligibility only, explicit `ec2` is accepted, and any other explicit family fails. The database is `db.r5.large`; absent `serviceFamily` is inferred as `rds` for eligibility only, explicit `rds` is accepted, and any conflicting explicit family fails. MySQL workload identity is required on that database: use `workloadDatabaseEngine: "mysql"` or the narrow literal `engine: "mysql"` alias described above. The alias is accepted on that database shape even in an incomplete graph but does not by itself make the graph fit-eligible. The sole network node must have explicit `serviceFamily: "alb"`, or, only when serviceFamily is absent, an unambiguous name containing `ALB` or `Application Load Balancer`; any explicit size/type discriminator must not identify NLB or another network service. A generic unnamed network/load-balancer node, explicit NLB, or contradictory discriminator is ambiguous and fails eligibility. Each app may explicitly set `workload: "crud"` or omit the flag only for this completely matched canonical graph with MySQL identity; this is a canonical CRUD modeling assumption, not proof of actual customer workload behavior, and the inference is identified in `calibrationEvidence.note`. Explicit conflicting workload values fail. No CRUD inference applies to similar-but-not-exact graphs, another instance type/provider, or an incomplete graph. Each app must have autoscaling disabled and fixed `minInstances=maxInstances=2`; traffic must be in the inclusive 10–1,000 RPS range. There must be no active failure and no failure, warmup, or snapshot metadata. App `capacityRps`, `requestsPerSecond`, non-default `maxThroughput`, and non-default `baseLatency` overrides disqualify the fit. If supplied, database `maxConnections` must be 500 and `effectiveMaxConnections` must be absent; if supplied, ALB `maxThroughput` must be 50,000. The canonical `us-east-2` / seed `20240601` pair is the parity fixture, not evidence that another region has been measured. The separate owned typical fit requires appWeight=typical and the same four-node topology, explicit workload="crud-typical", appRuntime="node", appWorkerCount=2 and appDbPoolSize=250 per compute node, workloadDatabaseVersion="8.0" and explicit maxConnections=500 on the MySQL database, and internal loadBalancerScheme on the ALB. Every node must declare location.regionKey="us-east-2" (stored as canonical "use2"), bounds must be fixed 2→2, and load must be 20–300 RPS. Missing identity is never inferred. Only measured 20/100/200 RPS rungs from campaign typical-v1-20260927c are fitted; 300 RPS is a holdout and 500 RPS is saturation context. Generic typical apps and the public lean benchmark do not receive this fit. For ineligible graphs, `calibrationEvidence.kind` is `modeled` and `calibrationEvidence.note` names every actual failed eligibility check in stable order, with resource identifiers and safe expected / actual values; it must not report only a generic “not applied”. Examples include missing or ambiguous ALB identity, wrong app size or family, non-fixed instance bounds, traffic outside the range, active failure, and conflicting workload identity. For eligible graphs, evidence identifies the owned calibration and states that latency is modeled on the in-VPC ALB path, not end-to-end. No other hardware has been validated. Lean owned evidence requires root appWeight=lean; the separate typical fit requires typical. Known-vCPU serving compute uses the workload model: proportional lean AWS M5 scaling is scaled from measured. At offered application RPS T its full-precision fitted baseline is P50 = max(1.45, 2.4833 - 0.0010466*T) ms and P95 = max(P50, 3.10, 4.3697 - 0.0012973*T) ms. Topology, congestion, storage, memory, routing and failure penalties follow this baseline. There is no generic 5 ms floor on lean scaled latency; 0.05 ms is only a physical/non-finite safety floor. Other providers and generic typical/heavy are reference estimates. An explicitly identified owned typical Node CRUD setup uses its own separate three-rung fit; it does not change the lean benchmark. Unknown-vCPU fallback retains explicitly unverified generic output with no external reference. See PredictionEvidence for assumption intervals.
              - …
            - `routedRps` number: Compute and Kubernetes only — effective requests per second routed to this resource during the most recent simulation step. Only present on compute and kubernetes resources in step responses (`POST /simulations/{id}/step`); absent on GET and on non-compute, non-kubernetes resource types. The value is a float (one decimal place) so per-node values sum exactly to the simulation-level traffic without integer rounding error. For an ALB topology with three nodes at 45 RPS total all three nodes report `routedRps: 15.0`; if one node is killed the two surviving nodes each report `routedRps: 22.5` (the killed node's share is redistributed to the survivors). A node that is failed or parked reports `routedRps: 0.0`. Kubernetes clusters in an ALB topology report the per-cluster share of the routed traffic.
            - `availabilityState` 'available' | 'degraded' | 'unavailable' | 'scaled_to_zero' | 'cold_start': Compute, Kubernetes, and Database only — explicit availability classification set by the step pipeline after traffic redistribution is finalised. Absent on resource types where availability is not tracked (network, storage, cache, queue, security). `"available"` — the resource is healthy and serving traffic normally. `"degraded"` — the resource is under stress (`status: "critical"` or `"warning"`) but still serving positive traffic. `isRoutable` is `true`. A saturated DB with many open connections that is still handling requests correctly shows `"degraded"`, NOT `"unavailable"` — `status: "critical"` alone does not imply unavailability. `"unavailable"` — the resource is not serving any traffic: it is parked-critical (cpuUsage=0 and/or maxThroughput=0 while status=critical), downed by an AZ outage, or removed by an instance_kill failure. `isRoutable` is `false`. `"scaled_to_zero"` — an ECS Fargate service has no running tasks after scale-in. `"cold_start"` — an ECS Fargate service has no task ready to serve while tasks start. Both states have `isRoutable: false`; neither means a failed or parked resource. The field is optional for backward compatibility with stored snapshots that predate this field; it is always present on resources returned by step endpoints.
            - `isRoutable` boolean: Compute, Kubernetes, and Database only — whether this resource is receiving routed traffic during the current simulation step. `true` for resources with `availabilityState` of `"available"` or `"degraded"`. `false` for resources with `availabilityState` of `"unavailable"`, `"scaled_to_zero"`, or `"cold_start"`. Absent on resource types where availability is not tracked (network, storage, cache, queue, security), and absent on stored snapshots that predate this field.
            - `recoveryBlockedReason` string: When the recovery cooldown is not advancing because an engine guard blocked the demotion step, this field names the active guard. Absent when no guard is active and the resource is progressing normally through its cooldown arc. `"failure_park_window"` — the node was explicitly failed via injectNodeFailure and the minimum park-window has not yet elapsed; cooldownSteps is counting up toward the window boundary, not down toward healthy. `"idle_cpu_floor"` — the resource is parked-critical but the estimated idle CPU is still above the recovery threshold, so it cannot heal yet. The field is cleared (absent) on every step where neither guard fires.
            - `failureParkStepsRemaining` integer: Compute and Kubernetes only — number of simulation steps remaining in the forced unavailability window after an `inject-failure` call. Present only on step responses (`POST /simulations/{id}/step`); absent on GET responses, on resources that were not explicitly failed via `inject-failure`, and when the forced window has expired (value would be 0, so the field is omitted instead). The value counts down from `parkSteps - 1` on the first step after failure injection toward 0 on the last step of the park window, where `parkSteps` is `recoveryPolicy.failureParkSteps` when supplied and otherwise `recoveryPolicy.criticalSteps` (default 4, so the initial value is typically 3). Once the field is absent the node enters its organic cooldown-demotion recovery arc: it transitions critical → warning → healthy over subsequent steps.
            - `recoveryProgress` RecoveryProgress: Read-only recovery progress derived from the engine's current resource state. Poll `GET /simulations/{simulationId}` or the next step response until `state` is `healthy`; `stepsToHealthy` is only a lower-bound estimate.
              - …
            - `recoveryPolicy` object: Per-resource recovery thresholds that control how quickly the simulation transitions a resource from critical → warning → healthy. The four cooldown fields default to the global values (criticalCpuThreshold: 80, criticalSteps: 4, warningCpuThreshold: 70, warningSteps: 3) when omitted. `failureParkSteps`, when supplied, controls only the bounded unavailability window for the quick `inject-failure` flow; after that window the node can rejoin routing and continues through organic critical → warning → healthy cooldown. It must be at least 1. Use the persistent `instance_down` failure plus `recover-resource` when the node must remain unavailable until an explicit recovery action. Stateless microservices can use lower thresholds and fewer steps to heal faster; databases can use higher thresholds and more steps for a stricter recovery window.
              - …
          - `resourceId` string: For remove_resource action, ID of resource to remove
          - `resourceType` 'compute' | 'database' | 'storage' | 'network' | 'security': For add_resource action, type of resource to add
          - `provider` 'aws' | 'gcp' | 'azure' | 'oci' | 'digitalocean': For add_resource action, cloud provider for the new resource
          - `targetSize` string: For add_resource action, size label for the new resource (e.g. "t3.micro" for aws)
          - `regionKey` string: For add_resource action, region key to assign to the new resource (e.g. "us-east-1" for aws). Must be a valid region for the specified provider. Returns UNKNOWN_REGION if invalid.
        - SetRecoveryPolicyParams: Parameters for set_recovery_policy action. Supports two call forms: **Per-resource form** — provide `resourceId` and the nested `recoveryPolicy` object to override healing thresholds on one specific resource. **Global flat form** — omit `resourceId` and pass the four threshold fields (`criticalCpuThreshold`, `criticalSteps`, `warningCpuThreshold`, `warningSteps`) directly at the top level to apply the same policy to every resource in the simulation at once. The nested `recoveryPolicy` object takes precedence over the flat fields if both are supplied.
          - `resourceId` string: ID of the resource to update. Omit to apply the policy globally to all resources (global flat form).
          - `recoveryPolicy` object: Recovery policy thresholds to apply (per-resource form). Takes precedence over flat threshold fields when both are provided.
            - `criticalCpuThreshold` number, required: CPU % above which a resource is considered critical
            - `criticalSteps` integer, required: Steps the resource must stay at critical CPU before recovery triggers
            - `warningCpuThreshold` number, required: CPU % above which a resource is considered in warning state
            - `warningSteps` integer, required: Steps the resource must stay at warning CPU before recovery triggers
          - `criticalCpuThreshold` number: CPU % above which a resource is considered critical (global flat form — applies to all resources when resourceId is omitted)
          - `criticalSteps` integer: Steps a resource must stay at critical CPU before recovery triggers (global flat form)
          - `warningCpuThreshold` number: CPU % above which a resource is considered in warning state (global flat form)
          - `warningSteps` integer: Steps a resource must stay at warning CPU before recovery triggers (global flat form)
  - `collapseThreshold` number: Fractional drop in reward that triggers `reward_collapse`. Default is 0.20 (collapse when mean eval reward is more than 20 % below training total reward).
  - `evalCostOverrides` object: Per-dimension cost override rates in USD per 1 000 RPS per hour. Supported keys: `egress` (any simulation with traffic > 0) and `cross_az_traffic` (simulations with resources in distinct Availability Zones). These rates inflate `costPerHour` before the reward cost score is computed, revealing policies that exploit unpriced cost blind spots.

## Response `200`

Eval episodes completed synchronously (only returned when `?sync=true` is set).

- RLEvalJobResult
  - `episodes` object[], required: Per-episode results in the same order as the request `actions` array
    - `episodeIndex` integer, required: Zero-based index of this episode in the request array
    - `stepsExecuted` integer, required: Number of actions executed (may be less than the action array length if the episode ended early)
    - `totalReward` number, required: Cumulative reward across all steps in this eval episode
    - `steps` object[]: Per-step error evidence from the replay; database pressure details omit zero-contribution databases.
      - `step` integer, required: One-based step number within this evaluation episode
      - `error_breakdown` ErrorBreakdown (unresolved $ref)
  - `meanEvalReward` number, required: Mean of `totalReward` across all eval episodes
  - `trainingTotalReward` number, required: Stored cumulative training reward from the environment (the baseline being compared against)
  - `collapseThreshold` number, required: The collapse threshold used for this evaluation (echoed from request)
  - `reward_collapse` boolean, required: `true` when `meanEvalReward` falls more than `collapseThreshold × |trainingTotalReward|` below `trainingTotalReward`. Indicates the policy exploits cost dimensions unpriced during training.

## Other responses

- `202`: Eval job accepted. The job is running asynchronously. Poll `GET /rl/environments/{environmentId}/eval-episodes/{jobId}` until `status` is `completed` or `failed`.
- `400`: Invalid request data — structured field-level error
- `401`: No credentials provided to an API-key-protected endpoint. The response body includes a machine-readable `apiKeyAuth` block pointing to the key creation endpoint so agents can self-serve an API key without consulting the spec.
- `403`: Access denied — caller does not own this RL environment
- `404`: Resource not found
- `429`: RL rate limit exceeded (5 000 req/hr)

---

[API](https://skmtc.dev/cloudworldmodel/apis/cloud-world-model-agent-api.md) · [All operations](https://skmtc.dev/cloudworldmodel/apis/cloud-world-model-agent-api/llms.txt) · [OpenAPI document](https://skmtc.dev/cloudworldmodel/apis/cloud-world-model-agent-api/revisions/ecc3ad1ff07f?raw)
