crouter
DocsSdkGuides

Apps on crouter cloud

When an app starts runs on a person's runtime and needs data back from the agent, read this because runs.parse returns a typed, validated result and the transcript is not a data channel.

An app holding a person's connection starts runs on their runtime with runs.start, follows them with runs.events or runs.reply, and reads them with runs.get. This page covers getting data back from a run. For connections and per-person access, see Apps with many users.

Getting a result back from a run

Pass an output_schema and call runs.parse. The runtime arms the schema on the run's root agent. The agent must submit a value that validates against it. The run settles with that value, and runs.parse returns it as output_parsed, typed from your schema:

import {z} from 'zod';

const Profile = z.object({
  display_name: z.string(),
  bio: z.string(),
  facts: z.array(z.string()),
});

const outcome = await crouter.runs.parse({
  prompt: 'Draft a public profile for me from my notes.',
  output_schema: Profile,
});

if (outcome.kind === 'result') {
  saveProfile(outcome.output_parsed); // {display_name, bio, facts}, typed from Profile
} else if (outcome.reason === 'declined') {
  // The agent could not honestly satisfy the schema.
  const {reason, code, retryable} = outcome.details ?? {reason: 'declined', code: 'declined', retryable: false};
  showDecline(reason, code, retryable);
} else {
  // canceled, deadline_exceeded, or runtime_error (details.crouter_reason)
  showFailure(outcome.reason);
}

The runtime validates the value before it records it. A submission that does not match is refused with its field errors and never appears in runs.get, an event, or a report, so the agent fixes it and resubmits. output_schema accepts a plain JSON Schema object or anything with toJSONSchema(), such as a Zod v4 object. The SDK sends it as a JSON object and validates nothing itself. A schema that is not an object, or is over 256 KB serialized, is refused with invalid_request and param: 'output_schema', and no run is created. A submitted result can be at most 1 MiB. The same value is in runs.get(runId).result, which runs.wait returns unchanged, and on the run's node.settled event.

Do not parse assistant text out of runs.trace to get data back. runs.trace is for showing a person the transcript. Its text is whatever the model said, not a value anything checked.

Showing a background run's progress

An app that lets a run work in the background and polls it reads runs.get(runId).activity for a progress indicator. It is null unless status is running, and otherwise says what the run's root agent is doing now:

phaseThe root agent is
startinglaunching; its first turn has not begun
thinkingreasoning, or waiting on the model before any output
toolcalling a tool: tool.name, and tool.summary (the command's first line or the path it acts on, null while the call is still being written)
writingstreaming its answer text
between_turnsbetween turns while the run still works: a child agent is running, a message is about to start the next turn, or the runtime is reminding it to submit its output_schema result
const run = await crouter.runs.get(runId);
const badge = run.activity?.phase === 'tool' ? `Running ${run.activity.tool.name}` : run.activity?.phase ?? run.status;

activity covers the root only, and it is read each time you call runs.get, so a phase shorter than your poll interval can be missed. last_activity_at is when any agent of the run last did something. Do not read runs.trace for progress: it returns every conversation. To show the text as it streams, follow runs.events or runs.reply instead of polling.

Bounding a run and handling run_idle

No run is bounded in time until deadline lands. To bound one, pass a signal to runs.parse or runs.wait. When it aborts, the helper rejects with the signal's reason and leaves the run alone. Call runs.cancel if you want the run ended:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error('profile took too long')), 10 * 60_000);
let runId: string | undefined;
try {
  const started = await crouter.runs.start({prompt, output_schema: Profile});
  runId = started.run_id;
  const run = await crouter.runs.wait(runId, {signal: controller.signal});
  // …read run.status, run.outcome, run.result
} catch (error) {
  if (controller.signal.aborted && runId) await crouter.runs.cancel(runId);
  throw error;
} finally {
  clearTimeout(timer);
}

runs.parse rejects with APIError code run_idle when the agent stopped without submitting. The runtime reminds the agent of the schema up to three times in a row, then leaves the run idle with the schema still armed. The run is untouched: send runs.message to nudge the agent, which resets the reminders, or end the run with runs.cancel. A question from the run rejects with waiting_on_user unless you pass onQuestion. With onQuestion, parse answers each open question once and keeps waiting.

On this page