Runyard Docs
Guides

Building agents on Runyard

Discovery surfaces, the API-first guarantee, and recommended etiquette for agents and third-party clients.

Runyard treats agents as first-class API consumers. This page covers how a client discovers what a Hub can do, what it may rely on, and how to be a good citizen.

Discovery surfaces

SurfaceAuthWhat it gives you
GET /llms.txtnoneStatic, generic orientation for agents: what Runyard is, its tool names, the run path. It deliberately contains nothing deployment-specific and points at the authenticated menu.
GET /openapi.jsonnoneThe full HTTP surface, generated from the Hub's API surface registry — it cannot drift from the routes the server actually registers.
MCP tools/listtokenThe complete MCP tool set (parity-tested against the API surface). Connect with runyard mcp install or the runyard-mcp stdio server.
GET /api/menu / get_menutokenThe live, private catalog: workflows with schemas, execution modes, runner pool state, and ready-to-use CLI/MCP invocations.

Start every session with get_menu (or GET /api/menu), then describe_workflow for anything you intend to run.

The live catalog is an intentional menu, not a dump of Hub internals. Discovery defaults to audience: "product" workflows. Admin-scoped operator clients may explicitly request operations and internal entries with includeAudience=operations,internal on GET /api/menu / GET /api/workflows, or the includeAudience argument on MCP get_menu, list_workflows, and search_workflows. Treat audience as catalog visibility only; it does not replace token scopes, approvals, secret grants, or workflow.adminOnly checks.

Launch plans

Before creating a run, ask Runyard to compile the workflow contract into one read-only launch plan. The plan includes required and optional input fields, deterministic preflight status, questions, suggested defaults, repo/project selector options, eligible hook profiles with readiness, matching runner availability, budget support, approval policy, relevant Learning Ledger hints when available, and the exact preflight and run payload shapes to submit next.

runyard plan idea-to-product -i '{"idea":"demo"}' --where remote --json
const agent = new AgentHubClient({ baseUrl, token });
const planned = await agent.planWorkflowRun({
  id: "improve",
  input: JSON.stringify({}),
  where: "local"
});

if (planned.launchPlan.status === "needs_input") {
  console.log(planned.launchPlan.questions);
  console.log(planned.launchPlan.repoSelector.options);
}

When launchPlan.learningHints.items[] is present, treat it as workflow-specific operational memory before choosing inputs or creating the run. Each hint is proposal-only and labels evidence separately from inference: linked run ids and evidence counts are historical ledger evidence, while recommendations and backtest impact are bounded estimates. Use higher-confidence, current-scope hints to avoid repeated failures or missing inputs; ignore absent hints, stale hints, low-confidence hints, or hints whose scope does not match the run you are about to start. Launch plans omit raw event payloads and evidence excerpts by default.

Planning never creates a run or a draft, and launchPlan.submission.submitRequiresFreshPreflight is always true: runners, secrets, hooks, and budgets can change between planning and submission.

The API-first guarantee

The web app is an ordinary client of the same HTTP API, with no privileged endpoints. Every route is declared in one registry that also generates openapi.json and is parity-tested against the MCP tool list — so a lossless third-party client is possible by construction: anything the web app can show or do, your client can too, over HTTP or MCP.

Headless SDK

Node ESM consumers can use the v0 headless SDK from the package root:

import { RunyardAgentClient } from "runyard";

const runyard = new RunyardAgentClient({
  baseUrl: process.env.RUNYARD_URL,
  token: process.env.RUNYARD_TOKEN
});

const started = await runyard.startWorkflow({
  workflow: "research",
  input: {
    title: "Research checkout latency",
    prompt: "Find the highest-leverage checkout latency fixes."
  },
  budget: { maxTokens: 200000, maxCostMicros: 2000000 },
  idempotencyKey: "checkout-latency-2026-08-03"
});

if (started.state === "needs_input") {
  console.log(started.negotiation.questions);
  console.log(started.draft?.id);
} else if (started.state === "blocked") {
  console.log(started.negotiation.blockers);
} else {
  let since = "";
  const runId = started.run.id;
  let final = null;
  while (!final?.handoff) {
    const page = await runyard.followTimeline({ runId, since, limit: 100 });
    since = page.nextSince || since;
    final = await runyard.getFinalHandoff({ runId });
    if (!final.handoff) await new Promise((resolve) => setTimeout(resolve, 2000));
  }
  console.log(final.handoff);
}

Mutating SDK helpers accept idempotencyKey. When omitted, the SDK generates one and returns it as idempotencyKey; persist that value and reuse it when retrying the same intent. On Hub-enforced high-risk routes (startWorkflow, submitRunDraft, and resolveApproval), reusing the same key with the same request replays the original response, while reusing it for a different request returns the Hub's 409 idempotency_key_conflict. Draft create/update helpers also send and expose the key for caller traceability, but the Hub does not yet replay those lower-risk draft edits. The SDK does not auto-approve, resume, retry, or choose a polling cadence; callers own those policy decisions.

The first stable SDK surface for this release is intentionally small: preflightWorkflow, createRunDraft, updateRunDraft, submitRunDraft, startWorkflow, followTimeline / getTimelinePage, getFinalHandoff, and resolveApproval. Other generated HTTP wrappers remain available as AgentHubClient and agentClientMethods, but the high-level lifecycle naming should be treated as the supported v0 contract.

StateMeaningSDK handling
readyPreflight has enough input and runner capacity to start.Returned by preflightWorkflow; startWorkflow then enqueues a run.
needs_inputThe Hub needs concrete answers before it can start.Returned with negotiation.questions and often a saved draft.
blockedA hard prerequisite is missing, such as no matching runner.Returned with negotiation.blockers; no run is created.
waiting_approvalA human decision card is holding the run.getFinalHandoff returns the current handoff and approval context; call resolveApproval only after a human decision.
pausedThe run is parked for an operator action, often quota or provider setup.getFinalHandoff returns the current handoff; fix externally, then use the normal resume API.
budget_exceededThe configured spend cap stopped the run.Terminal; getFinalHandoff returns the handoff pack.
succeededThe run completed successfully.Terminal; getFinalHandoff returns the handoff pack.
failedThe run ended with an error.Terminal; getFinalHandoff returns diagnostics and next action.
cancelledA user or policy cancelled the run.Terminal; getFinalHandoff returns the final lifecycle record.

Durable assistant handoff

For operator assistance, agents should use typed copilot conversations instead of parsing model-authored buttons. The preferred MCP flow is create_copilot_conversation -> send_copilot_message -> render the returned typed actions -> confirm_copilot_action when the operator approves. The HTTP flow is the same surface:

curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" \
  -H "Content-Type: application/json" \
  https://hub.example.com/api/copilot/conversations

curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $MESSAGE_KEY" \
  -d '{"text":"Prepare a gated implementation run for the docs fix."}' \
  https://hub.example.com/api/copilot/conversations/$CONVERSATION_ID/messages

curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" \
  -H "Idempotency-Key: $CONFIRM_KEY" \
  https://hub.example.com/api/copilot/conversations/$CONVERSATION_ID/actions/$ACTION_ID/confirm

Only actions returned by the Hub are executable; hallucinated or stale action ids are rejected server-side. Confirming the same stored action twice returns the stored outcome and creates at most one Runyard run. Read the conversation with get_copilot_conversation (or GET /api/copilot/conversations/{id}) to materialize terminal run results exactly once. ask_assistant is deprecated compatibility for simpler clients; it wraps the typed copilot message endpoint, creates or reuses a conversation, and returns typed actions instead of legacy browser button actions.

Auth expectations

Ask the Hub's administrator for a scoped access token (admins issue them from the Tokens page or POST /api/tokens; see Connecting agents). Agents typically need api and/or mcp — nothing more. Send it as Authorization: Bearer <token> on every HTTP call, or configure it for the MCP server. Never ask for or expect the bootstrap/admin token.

  • Set input.title. For agent-created runs, include a short human-readable title ("Audit checkout flow mobile states") so run lists, approval cards, and human handoff stay decipherable.
  • Preflight first. When input is rough or unverified, call preflight_workflow (POST /api/workflows/{id}/preflight) or pass negotiate: true to run_workflow. You get ready | needs_input | blocked with concrete questions and blockers instead of a run that fails.
  • Use run drafts for question/answer loops. create_run_draft creates a proposed run and preflights it; update_run_draft merges answers into the input and re-preflights; submit_run_draft enqueues the real run only once it is ready; discard_run_draft abandons it. Nothing executes until submit.
  • Fetch the handoff pack when a run stops. After run_workflow --follow (or a terminal SSE frame), call get_run_handoff (GET /api/runs/{id}/handoff) for the bounded summary a parent agent needs: lifecycle status, next action, approvals, usage/budget, output keys and safe scalar fields, top artifact download links, diagnostics, flow, work-item linkage, and deep links. Use raw endpoints only when you need deeper detail.
  • Poll the timeline with since cursors while the run is active. get_run_timeline (GET /api/runs/{id}/timeline) returns a unified, ascending view of status transitions, events, and artifacts. Pass the previous response's nextSince as since so the Hub never re-sends what you have.
  • Cap what a run may spend. Pass budget: { maxTokens, maxCostMicros } when starting a run; a breach hard-stops the run as the distinct terminal status budget_exceeded (never a silent overrun). Read spend as you go from the run's usage and budgetStatus fields, and use get_usage_summary for a fleet rollup. See Usage metering & budgets.
  • Expect paused runs, and resume them. A run interrupted by exhausted provider credits/quota parks as paused instead of failing. Check run.pause for the reason and required action, fix it, then resume_run (POST /api/runs/{id}/resume) — it continues from the engine checkpoint when one exists.
  • Triage with the attention queue. list_attention_runs (GET /api/runs/attention) returns everything waiting on a human — paused, awaiting approval, or recently budget-stopped — with counts. Poll it instead of scanning full run lists for stuck work.

A metered, resumable run end to end:

# Start with a hard budget (~$2 or 200k tokens, whichever comes first)
curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" -H "Content-Type: application/json" \
  -d '{"input": {"title": "Deep-dive: checkout latency"}, "budget": {"maxTokens": 200000, "maxCostMicros": 2000000}}' \
  https://hub.example.com/api/workflows/research/run

# Watch spend vs limit
curl -H "Authorization: Bearer $RUNYARD_TOKEN" https://hub.example.com/api/runs/$RUN_ID/usage

# If the provider account runs dry the run parks as "paused" — fix, then:
curl -X POST -H "Authorization: Bearer $RUNYARD_TOKEN" https://hub.example.com/api/runs/$RUN_ID/resume

# Anything else waiting on a human?
curl -H "Authorization: Bearer $RUNYARD_TOKEN" https://hub.example.com/api/runs/attention

# After follow finishes, get one bounded handoff for final reporting or continuation
curl -H "Authorization: Bearer $RUNYARD_TOKEN" https://hub.example.com/api/runs/$RUN_ID/handoff

Push instead of poll: response endpoints

POST /api/workflows/{id}/run accepts an optional responseEndpoint ({ "type": "http" | "telegram", "config": { ... } }). When the run reaches a terminal state, the Hub POSTs a sanitized payload to your HTTP endpoint (or sends a concise Telegram message). Delivery state is visible on GET /api/runs/{id} under responseEndpoints[], and polling remains the canonical fallback.

Machine intake: workflow endpoints

For fixed-purpose integrations that should not hold a Hub token at all (webhooks, form intake, external systems), an admin can configure a workflow endpoint: POST /api/workflow-endpoints/{slug} accepts a payload authenticated with that endpoint's own secret, rate-limited, size-capped, and deduplicated, and triggers exactly one preconfigured workflow. This is the narrowest possible credential to hand to an external system.

On this page