Skip to main content
🚨Early AccessYour AI Team Just Got Smarter. Unlock Opus 5.5, Fable 5.1 & Sonnet 5.5, GPT-6 Astra GPT-6/6.1 Sol & GPT-6 Luna, Gemini 4 Argon with Team or Business.
Help Center / AI Agents / Connecting a self-hosted Runtime (Remote API)
AI Agents

Connecting a self-hosted Runtime (Remote API)

Updated · 9 min read

The Remote API is for agents that live entirely outside Orvoq — a LangGraph graph, a CrewAI crew, or any custom framework you run on your own infrastructure — and call into Orvoq to report what they are doing. Unlike a native or SDK-connected agent, Orvoq never needs a URL to reach your agent; your agent reaches Orvoq instead, which is why this works from inside a private network, a CI runner, or a sandbox with no inbound access at all.

Every call is plain REST, so it works from any language. The examples below use Python (via requests) since that is the most common runtime for LangGraph/CrewAI-style agents, but the same calls work from curl, Node, Go, or anything else that can send an HTTP request.

Before you start

  1. Register the agent.Agents page → “+ Register Agent” → choose Remote runtime as the runtime type. Connection URL is optional here — leave it blank unless you also want Orvoq to be able to dispatch toyour agent; a remote-runtime agent that only calls into Orvoq doesn't need one.
  2. Get it approved. A workspace owner activates the agent from the Agents page before it can spawn executions.
  3. Create an API key.Settings → API Keys → “Create API key”, with at least the permission scopes your agent needs (Sessions, Artifacts, Tasks, Decisions). The full key is shown exactly once — copy it immediately.
  4. Collect your IDs. Organization ID (Settings → Organization), Workspace ID (Settings → Workspace), and Agent ID (Agents page → open the registered agent → copy the ID from the URL).
A self-hosted infrastructure deployment can also authenticate with a static REMOTE_API_KEYS environment variable on the agent-service instead of a per-user API key — useful for a service account shared across many agents. If neither a database API key nor REMOTE_API_KEYS is configured, the Remote API refuses every request rather than silently allowing them through.

Authentication

Every request needs your key in one of two headers, against the /remote/v1 base path:

Headers
Authorization: Bearer YOUR_API_KEY
# — or —
X-Remote-Key: YOUR_API_KEY

Step 1 — Spawn an execution

Spawning creates the run your agent's work will be tracked under. Two modes: start it immediately, or leave it queued until you're ready.

python
import requests

NEXUS_URL = "https://your-workspace.orvoq.ai/remote/v1"
HEADERS = {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
}

resp = requests.post(f"{NEXUS_URL}/executions", headers=HEADERS, json={
    "organizationId": ORG_ID,
    "workspaceId": WORKSPACE_ID,
    "agentId": AGENT_ID,
    "label": "Customer feedback analysis",
    "initialTask": "Analyze Q3 feedback and identify top 5 issues",
    "autoStart": True,           # False leaves it "Queued" until you resume it
    "memoryScope": "execution",  # 'execution' | 'session' | 'workspace'
})
execution = resp.json()
EXECUTION_ID = execution["_id"]
SESSION_ID = execution["sessionId"]  # a headless session, auto-created for this run

This creates a standalone execution with its own headless session — it shows up on the Runs page immediately. If your agent is already a participant in a real session, spawn into that session instead with POST /sessions/:sessionId/executions.

Step 2 — Run tools through it

There are two distinct ways to invoke a tool, depending on who should be in control:

ModeEndpointUse for
Report-only/tools/reportYour agent already ran the tool itself — you just want it recorded for audit and monitoring.
Gate-then-execute/tools/invokeYou want Orvoq's permission and approval system to decide before anything runs — for anything dangerous (email, delete, deploy, write to a CRM).
python — report a tool your agent already ran
requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/report", headers=HEADERS, json={
    "toolName": "web_search",
    "input": {"query": "customer benchmarks 2026"},
    "output": search_results,
    "durationMs": 1200,
    "status": "success",   # or "failure", with an "error" field
})
python — ask permission before running a side-effecting tool
resp = requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/invoke", headers=HEADERS, json={
    "toolName": "send_email",
    "input": {"to": "team@company.com", "subject": "Q3 analysis", "body": "..."},
    "isSideEffecting": True,   # triggers the approval check
    "framework": "langgraph",
})
outcome = resp.json()

if outcome["status"] == "executed":
    print(outcome["result"])          # ran immediately, no approval needed
elif outcome["status"] == "blocked":
    print(outcome["reason"])          # tool not granted to this agent
elif outcome["status"] == "awaiting_approval":
    approval_id = outcome["approvalRequestId"]   # see Step 3

A framework that raises its own human-in-the-loop pause — a LangGraph interrupt(), for instance — without going through a specific tool call can open one directly with POST /executions/:id/request-approval, giving it a summary and the proposedchange. This blocks the execution (moving it to “Waiting on approval”) and surfaces it in Command Center exactly like a gated tool call.

Step 3 — Handle the approval pause

When a call comes back awaiting_approval, the execution is paused until a human acts in the Orvoq UI. Since your agent is outside Orvoq, it polls for the resolution rather than waiting on a webhook:

python
import time

def wait_for_approval(approval_id, timeout_seconds=300):
    start = time.time()
    while time.time() - start < timeout_seconds:
        status = requests.get(
            f"{NEXUS_URL}/approvals/{approval_id}/status", headers=HEADERS
        ).json()
        if status["status"] != "pending":
            return status["status"]  # 'approved' | 'rejected' | 'modified' | 'overridden'
        time.sleep(2)
    return "timeout"

resolution = wait_for_approval(approval_id)

if resolution == "approved":
    # Re-invoke with the resolved approval id so the gate isn't re-triggered
    requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/invoke", headers=HEADERS, json={
        "toolName": "send_email",
        "input": {"to": "team@company.com", "subject": "Q3 analysis", "body": "..."},
        "isSideEffecting": True,
        "framework": "langgraph",
        "resolvedApprovalId": approval_id,
    })
elif resolution == "modified":
    pass  # the reviewer changed the instructions — re-plan around them
elif resolution == "rejected":
    pass  # skip the action

In the Orvoq UI, the run shows a “Waiting on approval” badge the moment this happens, and the reviewer can Approve, Reject with a reason, Modify the instructions, or Override the output directly — the same four actions available for a native agent's approval requests.

Step 4 — Monitor and control the run

Poll the execution's current state, or subscribe to a live stream of its step count and token usage:

python
# Point-in-time status
status = requests.get(f"{NEXUS_URL}/executions/{EXECUTION_ID}", headers=HEADERS).json()
print(status["status"])  # Queued, Running, WaitingOnApproval, Completed, Failed, Killed, Retrying

# Real-time, via Server-Sent Events (updates roughly every 500ms)
# GET /executions/{EXECUTION_ID}/monitor  →  event stream of {status, totalSteps, totalTokens}

Lifecycle actions all take the same small body — who is acting, and why:

EndpointWhat it does
/executions/:id/suspendSoft-pause — keeps state, can be resumed later
/executions/:id/resumeResume a suspended or queued execution
/executions/:id/retryRetry after a failure
/executions/:id/cloneRestart fresh from the same configuration
/executions/:id/completeMark it successfully finished
/executions/:id/killHard stop — use when the work should end, successful or not
requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/complete", headers=HEADERS, json={
    "actorId": "my-orchestrator",
    "actorType": "system",
    "reason": "Task finished successfully",
})

Full endpoint reference

All paths below are relative to /remote/v1.

MethodPathDescription
POST/executionsSpawn a standalone execution (auto-creates a headless session)
POST/sessions/:sessionId/executionsSpawn an execution inside an existing session
GET/executions/:idGet execution details and status
POST/executions/:id/tools/reportRecord a tool call the agent already ran
POST/executions/:id/tools/invokeGate a tool through permission/approval before it runs
POST/executions/:id/request-approvalRaise a human review pause directly, without a specific tool call
GET/approvals/:id/statusPoll an approval's resolution
POST/approvals/:id/approveApprove programmatically
POST/approvals/:id/rejectReject programmatically, with a reason
POST/approvals/:id/modifyRedirect the agent with modified instructions
POST/approvals/:id/overrideSubstitute your own output for the proposed one
GET/sessions/:id/approvalsList pending approvals for a session
SSE/executions/:id/monitorLive execution monitoring stream
GET/sessions/:id/monitorSnapshot of every execution in a session
POST/executions/:id/suspendSoft-pause
POST/executions/:id/resumeResume
POST/executions/:id/retryRetry after failure
POST/executions/:id/cloneRestart fresh
POST/executions/:id/completeMark completed
POST/executions/:id/killHard stop
POST/sessions/:sessionId/:actionSession-level pause / resume / stop / restart / archive

Troubleshooting

ErrorCause / fix
401 — Missing API keyNo Authorization or X-Remote-Key header was sent.
401 — Remote API is not enabledNo API key exists for this organization and REMOTE_API_KEYS is unset on the agent-service — create one in Settings → API Keys.
401 — Invalid API keyThe key doesn't match any active database key or configured static key — check for typos, revocation, or expiry.
Tool returns blockedThe tool isn't in the agent's granted list — update its permissions or registration.
Approval never resolvesNo one is watching the run — approvals escalate after the configured timeout, but check the Runs page.
The Remote API routes every tool call through the same permission, approval, and audit engine a native or SDK-connected agent uses — nothing bypasses human oversight just because the framework lives on your own infrastructure. Every operation produces an audit record, and side-effecting calls checkpoint before they run.

Was this article helpful?