Skip to content

Operations Contract

Joseph T. French edited this page Sep 23, 2026 · 1 revision

Operations Contract

Every write in RoboSystems is a named operation, and every operation shares one contract: the same URL shape, the same response envelope, the same retry key, and the same progress stream. This page is the one home for that contract. The pages for each surface — Graph Operations, RoboLedger Operations, RoboInvestor Operations — cover what each operation does and link back here for how they all behave.

Table of Contents

Where Operations Live

Two families of operations share the contract:

POST /v1/graphs/{graph_id}/operations/{operation_name}                  # graph lifecycle and content
POST /extensions/{roboledger|roboinvestor}/{graph_id}/operations/{operation_name}   # extension domains

The first carries graph lifecycle and content writes (create-subgraph, create-backup, materialize, create-file-upload, ingest-file, and the rest). The second carries the RoboLedger and RoboInvestor command writes (create-event-block, close-period, create-security) and the analytical view operations (build-fact-grid, financial-statement-analysis), which are read-only but ride the same envelope.

In both, graph_id is a URL path parameter, authentication is the X-API-Key header, and access to the graph is checked before the operation runs. The body is JSON. Reads are not operations: they are REST GETs under /v1 or GraphQL at /extensions/{graph_id}/graphql.

The per-operation request and response models are in the API reference.

The OperationEnvelope

Every operation that succeeds returns an OperationEnvelope. On the wire its field names are camelCase:

Field Type Meaning
operation string The kebab-case operation name, e.g. create-event-block
operationId string op_ followed by a 26-character ULID. The correlation key for the progress stream, the status snapshot, and the audit log
status string completed, pending, or failed
result object, list, or null The operation's payload. Its shape is per operation and is typed in the API reference
at string ISO-8601 UTC timestamp with a Z suffix, to the second
createdBy string or null The id of the user whose credentials made the call
idempotentReplay boolean true when this envelope came from the idempotency cache and the operation did not run again

The payload inside result keeps the operation's own field names, which are snake_case (external_id, posting_date). Only the envelope's own keys are camelCase.

A synchronous success:

{
  "operation": "create-event-block",
  "operationId": "op_01K5Q3Z8W4N2B7C1D9E6F0G3H5",
  "status": "completed",
  "result": {
    "id": "evt_01K5Q3Z8X2M4P6R8T0V2W4Y6Z8",
    "event_type": "invoice_issued",
    "status": "captured",
    "source": "driftline",
    "external_id": "drf_inv_10042",
    "amount": 480000,
    "currency": "USD"
  },
  "at": "2026-09-22T14:05:11Z",
  "createdBy": "user_abc123",
  "idempotentReplay": false
}

(The result above is trimmed; an event block carries more fields.)

Sync and Async

Each operation either finishes inside the request or hands its work to a background worker.

  • Synchronous operations return status: "completed" with the output in result, normally over HTTP 200.
  • Asynchronous operations return status: "pending", normally over HTTP 202. result carries what the operation knows at hand-off — an acknowledgement message and, for some operations, a monitoring.sse_endpoint — and operationId is the handle you follow.

A pending envelope:

{
  "operation": "create-backup",
  "operationId": "op_01K5Q41B7C9D2E4F6G8H0J2K4M",
  "status": "pending",
  "result": {
    "status": "accepted",
    "message": "Backup creation started",
    "retention_days": 30,
    "monitoring": { "sse_endpoint": "/v1/operations/op_01K5Q41B7C9D2E4F6G8H0J2K4M/stream" }
  },
  "at": "2026-09-22T14:07:40Z",
  "createdBy": "user_abc123",
  "idempotentReplay": false
}

Branch on the envelope's status, not on the HTTP code. The two usually agree, but not always: a materialize dry run returns a completed envelope under the route's 202, and create-subgraph with fork_parent: true returns a pending envelope under 200. Some operations are sync or async depending on the request; the envelope always tells you which one you got.

When an Operation Fails

failed is a legal envelope status, but over REST you meet failure in two other shapes, depending on when it happens.

Refused before or during a synchronous run — validation, access, a domain rule, a conflict. The response is an HTTP error with the platform's standard error body, not an envelope:

{
  "detail": "Cannot write to closed period '2026-08' (posting_date=2026-08-14). Reopen the period first if an adjustment is needed.",
  "request_id": "7f3c9a1e-2b4d-4c6e-8f0a-1b2c3d4e5f60"
}

Nothing was written: a synchronous operation that raises rolls back its whole unit of work. The status codes, the detail shapes, and what to do about each are in Errors and Rate Limits.

Failed after an async hand-off. The request already returned a pending envelope, so the failure arrives on the operation's progress stream as an operation_error event, and the status snapshot reports "status": "failed" with an error message — see Following an Async Operation.

Idempotency-Key

Every operation accepts an optional Idempotency-Key header. Without one, a retried POST runs the operation again. With one, the platform recognises the retry:

Situation Response
First request with this key The operation runs; its envelope is stored under the key
Same key, identical body, after the first finished The stored envelope, with idempotentReplay: true. The operation does not run again
Same key, identical body, while the first is still running 409 Conflict — the first request is in progress; retry after it completes to receive its result
Same key, different body 409 Conflict — the key is bound to the first body it saw. Use a fresh key for a different request
First request failed (an HTTP error) Nothing is stored; a retry with the same key runs the operation again
First request returned pending and the background work later failed or was cancelled The stored envelope is evicted, so a retry with the same key dispatches again rather than replaying pending

The details that matter when you design retries:

  • Window. A stored envelope is kept for 24 hours.
  • Scope. A key is scoped to the calling user, the graph, and the operation name. The same key string used by two users, on two graphs, or for two operations never collides.
  • Body identity. The comparison is over the parsed JSON body, so key order and whitespace do not matter; any change of value does.
  • What a replay returns. The original envelope, including its original operationId, at, and result, with idempotentReplay set to true. For an async operation the replay is the original pending envelope — follow its operationId for the outcome.
# Safe to re-run: the second call replays the first envelope instead of creating a second backup
curl -s -X POST "https://api.robosystems.ai/v1/graphs/$GRAPH_ID/operations/create-backup" \
  -H "X-API-Key: $ROBOSYSTEMS_API_KEY" \
  -H "Idempotency-Key: nightly-export-2026-09-22" \
  -H "Content-Type: application/json" \
  -d '{"backup_format": "full_dump", "retention_days": 30}'

A good key names the intent of the request — a source record id plus a version, or a job id plus a date — so the retry after a network error carries the same key and a genuinely new request carries a new one.

Idempotency-Key is separate from any idempotency an operation has in its own data. create-event-block, for example, also refuses a second event with the same source and external_id, with or without the header — see Build a Ledger Integration.

Following an Async Operation

A pending envelope's operationId works with two endpoints, both under /v1:

GET /v1/operations/{operation_id}/stream    # Server-Sent Events (text/event-stream)
GET /v1/operations/{operation_id}/status    # point-in-time JSON snapshot

Both are authenticated with X-API-Key, answer only for operations you started, and consume no credits. The stream endpoint allows a small number of concurrent connections per user (the limit is in its description in the API reference); close streams you are done with.

The stream

curl -N "https://api.robosystems.ai/v1/operations/op_01K5Q41B7C9D2E4F6G8H0J2K4M/stream" \
  -H "X-API-Key: $ROBOSYSTEMS_API_KEY"

(-N turns off curl's buffering so events print as they arrive.)

Each event is a standard SSE frame: an event: name and a single-line JSON data: payload. Operation events carry operation_id, timestamp, and a sequence_number alongside their own fields:

event: connected
data: {"operation_id": "op_01K5Q41B7C9D2E4F6G8H0J2K4M", "connection_id": "0b6e…", "from_sequence": 0}

event: operation_progress
data: {"operation_id": "op_01K5Q41B7C9D2E4F6G8H0J2K4M", "timestamp": "2026-09-22T14:07:52Z", "sequence_number": 3, "message": "Exporting database", "progress_percent": 40}

event: operation_completed
data: {"operation_id": "op_01K5Q41B7C9D2E4F6G8H0J2K4M", "timestamp": "2026-09-22T14:09:03Z", "sequence_number": 6, "message": "Operation completed successfully", "result": {"backup_id": "bk_…"}}

event: stream_end
data: {"status": "completed", "operation_id": "op_01K5Q41B7C9D2E4F6G8H0J2K4M", "message": "Stream ended normally"}

The operation events:

Event Meaning Payload fields beyond the common three
operation_started A worker picked the operation up message, progress_percent (0)
operation_progress An incremental update message, progress_percent (0–100, may be null), plus operation-specific detail
operation_completed Finished; terminal message, result
operation_error Failed; terminal error, error_details
operation_cancelled Cancelled; terminal
operation_awaiting_input Paused at a checkpoint for a decision; ends the stream until resumed
operation_resumed A paused operation was resumed

The stream also emits framing events: connected when it opens, keepalive during quiet periods, stream_end after a terminal event, and error if the stream itself cannot serve the operation (unknown id, not yours). A stream opened on an operation that has already finished replays its whole history and then ends.

Reconnecting. Events are stored with their sequence numbers for a limited time, so a dropped connection loses nothing. Reconnect with from_sequence set to one more than the last sequence_number you saw; the stream replays everything from that number (inclusive) before going live:

curl -N "https://api.robosystems.ai/v1/operations/op_01K5Q41B7C9D2E4F6G8H0J2K4M/stream?from_sequence=4" \
  -H "X-API-Key: $ROBOSYSTEMS_API_KEY"

With the default from_sequence=0, a stream on a running operation shows only what happens from now on; pass from_sequence=1 to replay from the start.

The status snapshot

If you would rather poll:

curl -s "https://api.robosystems.ai/v1/operations/op_01K5Q52D4F6H8J0K2M4N6P8R0S/status" \
  -H "X-API-Key: $ROBOSYSTEMS_API_KEY"
{
  "operation_id": "op_01K5Q52D4F6H8J0K2M4N6P8R0S",
  "operation_type": "extensions_materialize",
  "status": "failed",
  "created_at": "2026-09-22T14:07:40Z",
  "updated_at": "2026-09-22T14:08:15Z",
  "graph_id": "kg1a2b3c4d5e6f7890",
  "error": "",
  "message": "Operation execution failed",
  "_links": { "stream": "/v1/operations/op_01K5Q52D4F6H8J0K2M4N6P8R0S/stream" }
}

operation_type names the background task that ran the work, which is not always the operation's own name.

status is one of pending, running, awaiting_input, completed, failed, or cancelled. A completed operation carries result; a failed one carries error. While the operation is pending the snapshot also reports queue_position and queue_depth, and while it can still be stopped _links includes cancelDELETE /v1/operations/{operation_id} cancels a pending or running operation.

Once an operation's records expire, both endpoints answer 404. Read the outcome while it is fresh, and treat the resource the operation produced (the backup, the materialized graph) as the durable record.

Client Checklist

  • Send an Idempotency-Key on every write you might retry, and reuse it only for the same request.
  • Branch on status in the envelope: completed → read result; pending → follow operationId.
  • Treat a 409 whose detail says the key is in progress as "wait and retry the same request", and one that says the key was reused with a different body as a bug in your key choice.
  • On a non-2xx response, log the request_id from the body (also sent as the X-Request-ID header) — see Errors and Rate Limits.
  • For async work, prefer the stream; fall back to polling /status with a delay; stop at a terminal state.

Self-hosted deployments

The contract is identical on a local stack at http://localhost:8000. Idempotency records and operation events live in the deployment's Valkey, so flushing its caches (for example with just admin dev cache flush) forgets stored envelopes and progress history. The Idempotency-Key behaviour is best-effort by design: if the cache is unreachable, operations still run rather than fail.

Related Documentation

Wiki Guides:

API Reference:

  • API reference - Every operation's request and typed result model

Support

Clone this wiki locally