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.

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.

const task = await runner.tasks.get(binding.taskId); const runId = task.metadata.active_run_id ?? task.metadata.latest_run_id; 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 task = await runner.tasks.get(binding.taskId); outcomes.feedback("thumbs_down", { conversationId: binding.taskId, previousResponseId: task.metadata.last_response?.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