Skip to Content

RL Sandbox

Use Veris as the environment layer for an RL training run you operate. Your policy and rollout framework keep their existing tool schemas, message formatting, and training loop. The tools call the same vendor SDKs they use in production, but those SDKs point at isolated Veris services instead of live vendors.

This integration works with SkyRL, NeMo Gym and NeMo RL, Tinker Cookbook, or a custom rollout system. It does not require a framework-specific Veris SDK.

This page is for teams bringing their own training stack. To run a Veris-managed GRPO training job, see Reinforcement Learning (GRPO).

Where Veris sits

policy output your framework step / agent loop your tool implementation Stripe, Gmail, Salesforce, or another vendor SDK ↓ base URL changed Veris sandbox

Veris sits at the vendor HTTP boundary. It does not implement a generic step() endpoint.

Your training stack ownsVeris owns
Sampling, tokenization, chat templates, and loss masksIsolated vendor-compatible services
Parsing model output and dispatching tool callsStateful, inspectable vendor data
Rollout grouping, retries, and asynchronous schedulingReusable starting worlds and deterministic IDs
Episode termination and advantage calculationVirtual time, request traces, and failure injection
Reward compositionGround-truth state for reward predicates

A tool can make several calls across several vendors, and the meaning of one model action differs between agents and frameworks. Keeping step() in the rollout framework preserves those semantics and avoids an extra network hop around every action.

Create a reusable starting world

An environment names the services in a task and points to a durable baseline. A sandbox is one disposable, mutable copy of that environment.

Create the baseline once, then start every rollout from it:

Create an environment

curl -X POST https://api.veris.ai/v1/environments \ -H "Authorization: Bearer $VERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "billing-operations-rl", "services": ["stripe", "google-gmail"] }'

Save the returned id as VERIS_ENVIRONMENT_ID.

Create a curator sandbox

curl -X POST \ "https://api.veris.ai/v1/environments/$VERIS_ENVIRONMENT_ID/sandboxes" \ -H "Authorization: Bearer $VERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ttl_minutes": 120}'

Poll the returned sandbox until its status is ready. The response contains a stable url, control_url, and env_hint for every service.

Arrange the starting state

Use vendor APIs to build the world exactly as a real integration would, or write setup data through the service control plane:

curl -X POST "$STRIPE_CONTROL_URL/veris/data" \ -H "Content-Type: application/json" \ -d '{ "data": { "customers": [{"id": "cus_training", "email": "ops@acme.test"}] } }'

Use GET {control_url}/veris/schema to discover valid entity types and fields. Application and agent code should use only the vendor API; /veris/* is for environment setup, inspection, and reward code.

Promote the world

curl -X POST \ "https://api.veris.ai/v1/environments/$VERIS_ENVIRONMENT_ID/sandboxes/$CURATOR_SANDBOX_ID/promote" \ -H "Authorization: Bearer $VERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"clock_restore": "frozen"}'

Promotion captures the service state, shared virtual clock, deterministic ID positions, and the optional Veris Postgres database into one pinned environment revision. Future sandboxes boot that revision. Other running sandboxes are not changed; treat the curator sandbox as capture-only and delete it after promotion.

Use clock_restore: "frozen" when identical logical time is part of the task. Frozen time also pauses scheduled callback delivery. Use "rebase" when the episode requires time to advance or webhooks to dispatch: the sandbox starts at the captured instant and then advances normally.

Run an episode

The following helper contains the complete framework-neutral integration. Create one instance for each active trajectory.

veris_rollout.py
import os import time import requests VERIS_API = os.getenv("VERIS_API_URL", "https://api.veris.ai") VERIS_KEY = os.environ["VERIS_API_KEY"] ENV_ID = os.environ["VERIS_ENVIRONMENT_ID"] AUTH = {"Authorization": f"Bearer {VERIS_KEY}"} class VerisRollout: def __init__(self, sandbox): self.id = sandbox["id"] self.services = {service["name"]: service for service in sandbox["services"]} @classmethod def start(cls, ttl_minutes=120, timeout_seconds=180): response = requests.post( f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes", headers=AUTH, json={"ttl_minutes": ttl_minutes}, timeout=30, ) response.raise_for_status() sandbox = response.json() try: deadline = time.monotonic() + timeout_seconds while sandbox["status"] != "ready": if sandbox["status"] == "failed": raise RuntimeError(sandbox.get("failure_reason") or "sandbox failed") if time.monotonic() >= deadline: raise TimeoutError(f"sandbox {sandbox['id']} did not become ready") time.sleep(1) response = requests.get( f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{sandbox['id']}", headers=AUTH, timeout=30, ) response.raise_for_status() sandbox = response.json() except Exception: requests.delete( f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{sandbox['id']}", headers=AUTH, timeout=30, ) raise return cls(sandbox) def url(self, service): return self.services[service]["url"] def snapshot(self, service): control_url = self.services[service]["control_url"] response = requests.post(f"{control_url}/veris/snapshot", timeout=30) response.raise_for_status() return response.json() def requests(self, service, limit=1000): control_url = self.services[service]["control_url"] response = requests.get( f"{control_url}/veris/requests", params={"limit": limit}, timeout=30, ) response.raise_for_status() return response.json()["requests"] def close(self): response = requests.delete( f"{VERIS_API}/v1/environments/{ENV_ID}/sandboxes/{self.id}", headers=AUTH, timeout=30, ) if response.status_code not in (204, 404): response.raise_for_status()

Point the existing SDK at the URL returned for that rollout:

tools.py
import stripe def configure_stripe(rollout): stripe.api_base = rollout.url("stripe") stripe.api_key = "sk_test_veris" def refund_charge(charge_id: str): return stripe.Refund.create(charge=charge_id)

Any well-formed sandbox credential works in the default permissive auth mode, so your integration can usually keep the credential shape it already uses. Enable strict auth in the starting world when credential behavior is part of the task.

At episode end, read vendor-side state and compute a reward:

reward.py
def score_refund(rollout, target_charge_id: str) -> float: state = rollout.snapshot("stripe")["state"] charge = next(c for c in state["charges"] if c["id"] == target_charge_id) refunds = [r for r in state["refunds"] if r["charge_id"] == target_charge_id] outcome = float(charge["refunded"] and len(refunds) == 1) # Optional process reward: discourage avoidable vendor errors and retries. trace = rollout.requests("stripe") avoidable_errors = sum(1 for request in trace if (request["status"] or 599) >= 400) return max(0.0, outcome - 0.05 * avoidable_errors)

POST /veris/snapshot is a state inspection endpoint, not the durable environment baseline. The promoted environment revision is the reusable starting point; a snapshot is the current state of one service in one live sandbox.

Framework adapters

The lifecycle is the same in every framework: create a sandbox at episode initialization, let your existing tool dispatcher call its service URLs, read state for reward, and delete the sandbox in a finally block.

RL framework interfaces change frequently. Pin the framework version used by your training job and adapt constructor or return types as needed. The Veris lifecycle and service URLs stay outside tokenization and model-specific code.

SkyRL environments expose the right boundary through init() and step(). Keep action parsing and tool dispatch in the SkyRL environment; Veris only supplies the service URL and ground truth.

skyrl_env.py
import json from skyrl_gym.envs.base_text_env import BaseTextEnv, BaseTextEnvStepOutput from reward import score_refund from veris_rollout import VerisRollout class StripeBillingEnv(BaseTextEnv): def __init__(self, env_config, extras): super().__init__() self.task = extras["task"] self.rollout = None def init(self, prompt): self.rollout = VerisRollout.start() configure_tools(stripe_base_url=self.rollout.url("stripe")) return prompt, {} def step(self, action: str) -> BaseTextEnvStepOutput: call = parse_tool_call(action) result = TOOLS[call.name](**call.args) reward = score_refund(self.rollout, self.task["target_charge_id"]) done = task_is_complete(self.rollout, self.task) return BaseTextEnvStepOutput( observations=[{"role": "tool", "content": json.dumps(result)}], reward=reward, done=done, metadata={"sandbox_id": self.rollout.id}, ) def close(self): if self.rollout: self.rollout.close()

Call close() from the rollout worker’s cleanup path, including failed and truncated trajectories.

Parallel rollouts

Allocate one sandbox per active trajectory. Never let two trajectories mutate the same sandbox.

Sampling patternSandbox allocation
Independent rolloutsOne sandbox per rollout
GRPO group of G completionsG sandboxes from the same environment baseline
Batched environment with B active slotsOne sandbox per active slot
Retried trajectoryA new sandbox from the same baseline

For a promoted environment, restoring the episode means deleting the used sandbox and creating another one. POST .../sandboxes/{id}/reset is reserved for profile-backed environments and returns 409 for a promoted baseline so it cannot silently replace the curated world with default seed data.

Do not promote or roll back an environment while a training run is active. Record the environment’s baseline.revision_id with the run configuration so the starting world is auditable alongside the model checkpoint.

Reward signals

Veris supports two complementary reward sources:

  • Outcome reward from POST {control_url}/veris/snapshot or paginated GET {control_url}/veris/data?entity_type=....
  • Process reward from GET {control_url}/veris/requests, including the vendor route, status, request and response bodies, and state version after each call.

For multi-service tasks, read each relevant service after the final tool call and combine the predicates in your reward code. Keep reward reads outside the agent’s tool surface so the policy cannot inspect ground truth directly.

Evidence for process graders

Veris records the vendor-side evidence from every rollout. Combine it with the trajectory captured by your training framework to evaluate how the agent reached an outcome.

process_grader.py
evidence = { # Captured by SkyRL, NeMo RL, Tinker, or your rollout loop "trajectory": framework_trajectory, # Captured by Veris "environment": { "stripe": { "requests": rollout.requests("stripe"), "final_state": rollout.snapshot("stripe")["state"], } }, } result = your_behavior_judge( behavior="Verify the customer before issuing a refund", evidence=evidence, ) # {"result": "pass", "reason": "Identity was verified before the refund call."}

Process graders can check whether the agent verified identity before a destructive action, recovered appropriately from a vendor error, or claimed success after the vendor rejected a request.

Veris does not run the judge or define the reward. It supplies deterministic state and request evidence; your training framework owns the behavior specification, judge model, and scoring.

Operational notes

  • Sandboxes are TTL-scoped. Delete them explicitly; TTL cleanup is a fallback.
  • Treat failed provisioning as terminal and preserve failure_reason in rollout metadata.
  • Use control_url for /veris/*. For HTTP services it equals url; for PostgreSQL, url is a database DSN and control_url is the HTTP inspection surface.
  • A frozen baseline keeps vendor time fixed and pauses callbacks. Advance or rebase time only when the task defines how time-bearing client protocols are handled.
  • Veris resets the dependency world. Your harness must separately reset the agent process, memory, caches, filesystem, and any database not hosted in the Veris sandbox.

API reference

EndpointPurpose
POST /v1/environmentsDefine a reusable set of services
POST /v1/environments/{env}/sandboxesCreate an isolated sandbox from the environment baseline
GET /v1/environments/{env}/sandboxes/{id}Poll readiness and read service URLs
POST /v1/environments/{env}/sandboxes/{id}/promoteCapture a curated sandbox as the environment baseline
DELETE /v1/environments/{env}/sandboxes/{id}Tear down one rollout sandbox
POST {control_url}/veris/snapshotRead one service’s current ground-truth state
GET {control_url}/veris/dataRead counts or paginated rows
GET {control_url}/veris/requestsRead the vendor request trace
GET {control_url}/veris/schemaInspect entity and field definitions for reward code