Run tasks from your backend
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
| Problem | Do this |
|---|---|
| A reload starts a second task | Save the task ID before streaming and reconnect with it. |
| Feedback missing from the conversation | Pass the task ID as conversationId and call flush(). |
401 after expires_in | Mint a new service-account token. |
| Resuming from a specific event | Not supported: the SDKs replay the whole run. |
| Service-account secrets in the browser | Never: mint a short-lived session instead. |