Skip to Content
Platform
How-toRun tasks from your backend

Run tasks from your backend

Call a deployed runtime from your server and tie user feedback to the answer it rates.

Your backend opens a runner for the signed-in user, starts a task, and streams it to the browser.

You need: a deployed runtime (here refund-agent from the Quickstart), the Node SDK (@introspection-sdk/introspection-node), a service account (Credentials), and a project API key in INTROSPECTION_API_KEY.

1. Open a runner for the user

Take the user ID from your server session, never from the request body.

import { IntrospectionClient } from "@introspection-sdk/introspection-node"; import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel"; const credentials = { clientId: process.env.INTROSPECTION_SERVICE_ACCOUNT_CLIENT_ID!, clientSecret: process.env.INTROSPECTION_SERVICE_ACCOUNT_CLIENT_SECRET!, project: process.env.INTROSPECTION_PROJECT!, }; const client = await IntrospectionClient.fromServiceAccount(credentials); const outcomes = new IntrospectionLogs({ token: process.env.INTROSPECTION_API_KEY, serviceName: "refund-app" }); // an API key: this client keeps one token for life const runner = await client.runtimes("refund-agent").run({ identity: { user_id: session.user.id }, });

The slug resolves to the version live in the credential’s environment. Tokens don’t refresh: re-mint before expires_in in a long-lived server. Create one client per process, and call runner.close() and client.shutdown() on exit.

2. Start the task and save its ID

Save the task ID before you stream; it is also the conversation ID.

// Your app's code: the order store and the push to the browser. const bindings = new Map<string, { taskId: string; userId: string }>(); const orders = { async saveAgentBinding(orderId: string, binding: { taskId: string; userId: string }) { bindings.set(orderId, binding); }, }; async function sendEventToBrowser(event: { type: string }) { console.log(`[${event.type}]`); } const handle = await runner.tasks.start({ prompt: "Review order 1842 against the refund policy and draft the next response.", metadata: { order_id: "1842" }, }); await orders.saveAgentBinding(order.id, { taskId: handle.run.task_id, userId: session.user.id }); for await (const event of handle.stream()) { await sendEventToBrowser(event); }

metadata reaches the agent as INTROSPECTION_TASK_METADATA_JSON.

3. Reconnect after a reload

Check the record belongs to the signed-in user, then replay the latest run from its start; skip events the browser already rendered.

// Your app's code: push the event to the browser. async function sendEventToBrowser(event: { type: string }) { console.log(`[${event.type}]`); } const task = await runner.tasks.get(binding.taskId); const runId = (task.metadata?.active_run_id ?? task.metadata?.latest_run_id) as string; for await (const event of runner.tasks.runs.stream(binding.taskId, runId)) { await sendEventToBrowser(event); }

4. Record the user’s feedback

Attach feedback to the conversation and the answer it rates. The answer’s ID is the task’s metadata.last_response.response_id.

const latest = await runner.tasks.get(binding.taskId); outcomes.feedback("thumbs_down", { conversationId: binding.taskId, previousResponseId: (latest.metadata?.last_response as { response_id?: string } | undefined)?.response_id, comments: "Missed the final-sale rule", }); await outcomes.flush();

Verify

introspection tasks get <task-id>

Expect your task, owned by the end user you passed as user_id. To stream straight to the browser instead, see Add a chat to your web app.

Or ask your agent: “Wire refund-agent into this app with a server route that starts, saves, and streams a task.”

Limits

ProblemDo this
A reload starts a second taskSave the task ID before streaming and reconnect with it.
Feedback missing from the conversationPass the task ID as conversationId and call flush().
401 after expires_inMint a new service-account token.
Resuming from a specific eventNot supported: the SDKs replay the whole run.
Service-account secrets in the browserNever: mint a short-lived session instead.
Last updated on