Skip to content
OpenTrain AIOpenTrain AIOpenTrain AIDocs

Ask OpenTrain

Answers from the documentation, with sources.

What would you like to do with OpenTrain?

AI answers can be mistaken. Check the linked sources. Don’t include private account information.

Open app

DEVELOPER DOCUMENTATION

Hosted Instructions visual proof

Render the real employer Instructions workspace or designated worker reader into agent-readable visual evidence.

Hosted Instructions visual proof lets an authorized employer or agent inspect the actual OpenTrain Instructions UI without running a local browser. OpenTrain captures the real employer workspace or a real designated test-worker reader in Chromium and returns private PNG, ARIA, diagnostics, and provenance artifacts.

Base path: /api/public/v1/instructions/visual

Agents should fetch the PNG and ARIA artifacts directly. Create an observe link only when an authorized human needs to inspect the same pinned render.

  1. Read capabilities to discover audiences, targets, viewports, retention, and worker-coverage rules.
  2. Create a session pinned to the job’s current canonical Instructions tree.
  3. Select a page or stable node, or include it when creating the session.
  4. Render desktop, mobile, or both using the current state revision.
  5. Inspect the PNG and semantic evidence directly, or mint a human review link.
  6. Revoke the link or expire the session when review is complete.
MethodPathPurpose
GET/capabilitiesDiscover the hosted visual-proof contract.
POST/sessionsCreate an employer or designated-worker session pinned to a job tree.
GET/sessions/{sessionId}Read session state and live source drift.
POST/sessions/{sessionId}/manageSelect a page/node, expire the session, or refresh its renderer grant with an expected-state revision.
POST/sessions/{sessionId}/renderCapture desktop/mobile evidence through the real product renderer.
GET/sessions/{sessionId}/artifactsList bounded private evidence records.
GET/artifacts/{artifactId}Read one artifact with a fresh signed resource URL.
POST/GET/sessions/{sessionId}/linksCreate or list observe-only links.
POST/sessions/{sessionId}/links/{linkId}/revokeRevoke one link.

Employer session:

{
"jobId": "job_123",
"audience": "EMPLOYER",
"pageId": "page_456"
}

Worker session:

{
"jobId": "job_123",
"audience": "WORKER",
"workerContractId": "contract_test_789",
"pageId": "page_456"
}

Worker proof is valid only through a real contract designated for testing. The response carries typed workerCoverage; no synthetic worker is substituted.

Render with the session’s current revision:

{
"expectedStateRevision": 2,
"viewports": ["desktop", "mobile"],
"pageId": "page_456",
"nodeId": "stable-section-id"
}

The response binds each artifact to the exact session, audience, source tree, page/node, viewport, render revision, checksum, and byte size. It also reports source drift against the live canonical Instructions. A successful render sets browserRenderVerified: true.

collaborationSyncVerified is always false. The capture proves what the renderer displayed at that revision; it cannot prove that an already-open collaborative browser received a real-time update. To test whether an open read-only view picks up a later change, use the live-propagation probe.

This section is about the proof image only. Instructions pages play YouTube and Loom videos as usual; the renderer does not load third-party frames, so the player area is blank in the PNG.

The renderer loads only OpenTrain’s own app and asset origins. It blocks third-party embeds, such as YouTube (www.youtube-nocookie.com) and Loom videos, by design. The embed area renders blank in the PNG, and the render still reports browserRenderVerified: true. The blocked address appears only in the diagnostics artifact, reduced to its origin and path. It is listed under blockedRequests, and also under failedRequests, because the browser reports a blocked request as failed.

The render result also counts the blocked embeds, with no address. When the count was measured and at least one embed was blocked, manifest.warnings carries a coded string for every caller:

THIRD_PARTY_EMBED_BLOCKED: 2 embedded third-party frame(s) were blocked by the renderer egress policy and render blank; redacted URLs are in the DIAGNOSTICS artifact.

To get the count as a typed field, send this header on the render request:

X-OpenTrain-Features: instructions-render-warnings-v1

The response then has a top-level renderWarnings list beside manifest:

{
"browserRenderVerified": true,
"collaborationSyncVerified": false,
"renderWarnings": [
{ "code": "THIRD_PARTY_EMBED_BLOCKED", "count": 2 }
]
}
renderWarningsMeaning
AbsentThe count was not measured for this response. Never read it as “nothing blocked”. A response without the header never has the field.
[]Measured: the renderer blocked no third-party embed.
[{ "code": "THIRD_PARTY_EMBED_BLOCKED", "count": 2 }]Measured: 2 embedded third-party frames were blocked and render blank.
  • code comes from a closed list. THIRD_PARTY_EMBED_BLOCKED is its only value today, and renderWarnings has at most one entry.
  • count is from 1 to 500. With more than one viewport, it is the highest count from a single viewport.
  • A warning never carries a URL, host, or query string. The redacted address stays in the diagnostics artifact.
  • The header changes only the response. A retry with the same Idempotency-Key returns the same saved render with or without the header, and it is not a key reuse. Render responses send Vary: X-OpenTrain-Features.
  • The manifest never has a renderWarnings field, so the inline manifest always matches the stored manifest artifact.

The probe checks whether a view that is already open shows a change that another writer commits while it watches. Add syncProbe to the render request. OpenTrain opens the session’s scoped read-only view once, records a baseline, and then only reads that same view. The probe never writes, reloads the page, or refreshes access. The CLI and MCP pages show the same probe on those surfaces.

{
"expectedStateRevision": 2,
"viewports": ["desktop"],
"syncProbe": {
"timeoutMs": 45000,
"expectedPageChecksum": "<64-hex page checksum>",
"expectedTreeChecksum": "<64-hex tree checksum>"
}
}
FieldRules
syncProbe.timeoutMsRequired. Integer from 5000 to 45000: how long the probe watches the open view, in milliseconds.
syncProbe.expectedPageChecksumOptional. 64 lowercase hex characters: the page checksum from your writer’s receipt.
syncProbe.expectedTreeChecksumOptional. 64 lowercase hex characters: the page-tree checksum from your writer’s receipt.

Before you call it:

  • Select a page. The session must have a page selected, either at session creation (pageId) or with manage select, and that page must belong to the session’s pinned tree. Otherwise the render fails with 409 and details.reason: "SYNC_PAGE_REQUIRED" before the probe starts, and the idempotency key stays unused. The probe never picks a page for you.
  • Use the desktop viewport only. Omit viewports or send ["desktop"]. Any other viewport list is rejected.
  • Start from a fresh session. The session’s pin is the baseline. If the page or tree already differs from it when the probe opens the view, the render fails with 409 and details.reason: "SYNC_BASELINE_CHANGED".
  • Run the writer separately. The render call is synchronous and has no “baseline ready” signal. Commit the change from another process while the render is running. A change that lands before the baseline is recorded returns BASELINE_MISMATCH, not a pass.
  • Allow up to two minutes. The CLI and the local MCP server wait up to 130 seconds for a probe render; give your own HTTP client at least that long.

The expected checksums tie the result to your writer’s commit. Without them, a verified result proves that a change reached the open view, not which writer made it.

A verified probe returns 200 with livePropagationVerified: true, propagationSurface: "SCOPED_READ_ONLY_VIEW", and a syncReport (the manifest carries the same report). Every other outcome returns 409 with details.reason: "COLLABORATION_SYNC_NOT_VERIFIED". Its details include the syncReport, the artifactIds of the saved evidence, and the sessionId.

409 (probe not verified)
{
"error": "The already-open view did not prove both body and tree propagation. Inspect the evidence; use a new session/key for a new experiment.",
"code": "CONFLICT",
"requestId": "<REQUEST_ID>",
"details": {
"reason": "COLLABORATION_SYNC_NOT_VERIFIED",
"syncReport": {
"status": "TIMEOUT",
"baseline": {
"pageId": "page_456",
"pageChecksum": "<PAGE_CHECKSUM>",
"treeChecksum": "<TREE_CHECKSUM>",
"bodyRenderHash": "<BODY_RENDER_HASH>",
"treeRenderHash": "<TREE_RENDER_HASH>",
"jobRevision": 7,
"folderRevision": 3
},
"observed": {
"pageId": "page_456",
"pageChecksum": "<PAGE_CHECKSUM>",
"treeChecksum": "<TREE_CHECKSUM>",
"bodyRenderHash": "<BODY_RENDER_HASH>",
"treeRenderHash": "<TREE_RENDER_HASH>",
"jobRevision": 7,
"folderRevision": 3
},
"elapsedMs": 45020,
"sameDocument": true,
"authFailureObserved": false,
"availabilityFailureObserved": false,
"unavailableReadCount": 0,
"firstFailure": null
},
"artifactIds": ["<ARTIFACT_ID>"],
"sessionId": "<SESSION_ID>"
}
}

syncReport.status is one of these values:

StatusMeaning
VERIFIEDThe open view showed the body and page-tree change, and a fresh authorized read of the canonical Instructions matched what it showed, including any expected checksums. This is the only success.
CANONICAL_MISMATCHThe view changed, but the fresh canonical read, the expected checksums, or another check did not match.
TIMEOUTNo complete, stable change arrived within timeoutMs. Images that never finish loading also end here.
BASELINE_MISMATCHThe open view did not match the session’s pin when the probe started, or changed while the baseline was captured.
AUTH_FAILUREA request for Instructions data from the open view returned 401 or 403 during observation.
AVAILABILITY_FAILUREA request for Instructions data from the open view failed with another HTTP error or a network error. Cancelled requests are ignored.
VIEW_NAVIGATEDThe open view navigated away.
VIEW_UNAVAILABLEAt least one read found the reader missing or hidden, or a fully loaded image broken.

The schema also lists OBSERVED. That is an intermediate renderer state; OpenTrain always resolves it to VERIFIED or CANONICAL_MISMATCH before it responds.

To reach VERIFIED, the page body and the visible page tree must both change in the same open document, and the job or folder revision must advance. The probe then needs three identical reads 250 ms apart within the window. Before it reports VERIFIED, OpenTrain re-checks the caller’s job access and, for a worker session, the worker’s coverage. A worker who lost coverage gets 409 with details.reason: "WORKER_COVERAGE_GAP".

Other report fields:

  • baseline and observed: the page ID, page and tree checksums, body and tree render hashes, and job and folder revisions. Either can be null when the probe did not get that far. The report carries digests and revisions, never page content.
  • sameDocument: whether the observed view is the same open document as the baseline.
  • authFailureObserved and availabilityFailureObserved: separate flags. Authorization failures (401 and 403) never count as availability failures, and the reverse. Once set, a later successful retry in the page cannot clear them.
  • unavailableReadCount: how many reads found the view unavailable.
  • firstFailure: the route template and HTTP status of the first failed request, or null. It never includes IDs or query strings. status is null for a network error.
  • elapsedMs: how long the probe ran.

livePropagationVerified is true only for a VERIFIED probe. It means the committed change appeared in a view that was already open, without a reload. That view is the scoped read-only view the session renders, named by propagationSurface: "SCOPED_READ_ONLY_VIEW".

collaborationSyncVerified stays false on every render, including a verified probe. Scoped views are read-only snapshots with no collaboration session, so the probe proves nothing about live collaborative editors.

Each probe is a single experiment:

  • A retry with the same Idempotency-Key returns the saved result, including a failed one. It never starts another observer.
  • If an earlier attempt with that key was interrupted before its evidence was complete, the render fails with 409 and details.reason: "SYNC_PROBE_ALREADY_STARTED". Inspect the session’s artifacts instead.
  • For a new experiment, create a new session, which pins a new baseline, and use a new idempotency key.

The evidence set has separate before and after PNGs, an ARIA snapshot, redacted diagnostics, and the manifest with syncReport. OpenTrain saves it before it returns a failed result.

  • Reads and artifact access require instructions:read.
  • Session control, rendering, and link mutations require instructions:write, an Idempotency-Key, employer job access, and the Instructions visual-proof rollout for the actor.
  • State-changing operations require the current expectedStateRevision and fail closed on drift.
  • Sessions, artifacts, and links are organization-bound, expiring, and revocable.
  • Visual proof writes only isolated preview evidence. It never changes an Instructions page or learner record.
  • Retry a transient renderer 503 with the same idempotency key.

@opentrain-ai/sdk 0.21.0 or later exposes getInstructionsVisualCapabilities, createInstructionsVisualSession, getInstructionsVisualSession, manageInstructionsVisualSession, renderInstructionsVisual, artifact list/get, and link create/list/revoke.

Version 0.33.0 or later accepts syncProbe in renderInstructionsVisual. Its default request timeout for a render is 120 seconds; pass a longer timeoutMs for a probe:

const result = await client.renderInstructionsVisual(
sessionId,
{ expectedStateRevision: 2, syncProbe: { timeoutMs: 45000 } },
{ idempotencyKey: "sync-probe-v1", timeoutMs: 130_000 },
);

A probe that is not verified throws OpenTrainApiError with status 409. Its details carry the reason, the syncReport, and the artifactIds.

Use the machine-readable OpenAPI document for complete request, response, and error schemas.