Drive Coder from your own code with CoderClient. Coder sessions run on a shared Hub, the real-time broadcast bus that apps, agents, and humans all subscribe to. The flow below creates a session, reads it back, and manages its lifecycle over time.
This flow works without deploying your app. If you already created an Agentuity project, its local .env usually includes AGENTUITY_SDK_KEY, which is enough for CoderClient discovery mode.
If discovery succeeds but session calls fail with an org-level error, your API key is fine. The org just needs Coder turned on first.
The examples below are standalone @agentuity/coder TypeScript snippets. Bun is the quickest way to run them locally. If you prefer Node, use tsx or another TypeScript runner.
The Pattern
Install the package:
npm install @agentuity/coder @agentuity/telemetryIf your app creates sessions from more than one place (an HTTP route, a background job, a webhook handler), construct a CoderClient once and share it:
import { CoderClient } from '@agentuity/coder';
export const coder = new CoderClient();With AGENTUITY_SDK_KEY set in your environment, this authenticates and discovers the Hub URL for your region. Pass orgId or region explicitly when your app needs to pin a specific one.
The standalone scripts on this page keep new CoderClient() inline so each one stays copy-pasteable. Move to a shared module once your app has more than one call site.
Start with a minimal script that covers discovery, session creation, and the first session read:
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
const DEFAULT_TASK = 'Inspect this repo and explain the current architecture.';
function getTask(): string {
const task = process.argv.slice(2).join(' ').trim();
return task || DEFAULT_TASK;
}
async function main(): Promise<void> {
// Let the SDK discover the Hub from AGENTUITY_SDK_KEY
const client = new CoderClient();
const task = getTask();
try {
const resolvedUrl = await client.getUrl();
logger.info('Connected to Coder Hub:', resolvedUrl);
const created = await client.createSession({
task,
workflowMode: 'standard',
tags: ['architecture', 'docs-example'],
});
logger.info('Created session:', created.sessionId);
// Read the session back to inspect the first lifecycle state
const session = await client.getSession(created.sessionId);
logger.info(
JSON.stringify(
{
sessionId: session.sessionId,
label: session.label,
status: session.status,
bucket: session.bucket,
workflowMode: session.workflowMode,
runtimeAvailable: session.runtimeAvailable,
controlAvailable: session.controlAvailable,
wakeAvailable: session.wakeAvailable,
historyOnly: session.historyOnly,
liveExpected: session.liveExpected,
},
null,
2
)
);
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
logger.error('Failed to create or inspect the session');
logger.error(message);
process.exitCode = 1;
}
}
void main();This gives you two things you need right away:
- a
sessionIdyou can reuse in later calls or other apps - the first session detail from
getSession()
What you should see
The exact values will vary, but a brand-new session usually looks like this:
{
"sessionId": "codesess_f64101b6ad47",
"label": "Inspect this repo and explain the current architecture.",
"bucket": "provisioning",
"workflowMode": "standard",
"runtimeAvailable": false,
"controlAvailable": false,
"wakeAvailable": false,
"historyOnly": false,
"liveExpected": true
}The Hub auto-derives label from the task prompt when the caller doesn't set one, so a fresh session always has a label to show in list views.
The exact status string varies and is not the field to branch on in this example. What matters is:
- creation succeeded and you have a real
sessionId getSession()returns usable session detail immediatelybucketand the availability/history booleans tell your app whether the session is still provisioning, attachable, wakeable, or replay-only
Creating Sessions from Your App
In app code, the shape you usually reach for adds three fields on top of task: label, metadata, and env. These cover the pieces apps actually need: a short label for list UIs, correlation ids that tie the session back to your own records, and any credentials the sandbox-side agent reads at runtime.
import { coder } from './coder';
type StartSessionInput = {
userId: string;
conversationId: string;
task: string;
apiToken: string;
};
export async function startSession(input: StartSessionInput) {
return coder.createSession({
task: input.task,
// label shows up in list UIs; set it when your task prompt is too long for display
label: input.task.slice(0, 100),
// metadata is for app correlation ids and app-owned session context
metadata: {
userId: input.userId,
conversationId: input.conversationId,
},
// env is what the sandbox-side agent can read at runtime
env: {
API_TOKEN: input.apiToken,
},
});
}| Field | What it carries |
|---|---|
task | The prompt the agent executes, including any context you want it to act on |
label | A short human string for list UIs and dashboards |
metadata | App correlation ids and app-owned session context |
env | Credentials and runtime config the sandbox-side agent reads |
When your app already resolved user or account context, the common split is: put resolved context into task, correlation ids into metadata, and integration credentials into env. env is fixed once the session is created, so prefer credentials the agent can refresh itself over short-lived access tokens.
Add an Output Contract
Coder sessions are long-running work, so make the expected result explicit before the session starts. A good task names the goal, inputs, files or reports, verification command, stopping rule, and missing-evidence behavior.
import { coder } from './coder';
type StartReleaseReviewInput = {
runId: string;
repoUrl: string;
branch: string;
files: readonly string[];
};
export async function startReleaseReview(input: StartReleaseReviewInput) {
const resultPath = `agent-output/${input.runId}/release-review.json`;
const reportPath = `agent-output/${input.runId}/release-review.md`;
return coder.createSession({
label: `Release review ${input.runId}`,
workflowMode: 'standard',
repo: {
url: input.repoUrl,
branch: input.branch,
},
metadata: {
runId: input.runId,
resultPath,
reportPath,
},
tags: ['release-review', input.runId],
task: [
'Goal: review the selected files for release blockers and risky assumptions.',
'',
'Files:',
...input.files.map((file) => `- ${file}`),
'',
'Write these artifacts:',
`- ${resultPath}`,
`- ${reportPath}`,
'',
'Contract shape:',
'{',
' "status": "completed" | "blocked",',
' "filesInspected": string[],',
' "findings": [{',
' "path": string,',
' "risk": string,',
' "sourceOfTruth": string,',
' "severity": "low" | "medium" | "high"',
' }],',
' "commandsRun": string[],',
' "missingEvidence": string[]',
'}',
'',
'Success criteria:',
'- Every finding cites a file path and source of truth.',
'- Missing evidence is listed instead of turned into a claim.',
'- The markdown report summarizes decisions, tradeoffs, and verification.',
'',
'Stop when:',
'- Both outputs are written, or a blocker is proven with the exact command, file, or API response.',
'',
'Do not include hidden reasoning. Summarize findings, decisions, evidence, and tradeoffs.',
].join('\n'),
});
}createSession() returns session state, not completed files. Keep the sessionId, store the expected output paths in your own app state, then check event history, replay, or artifacts until the output contract is satisfied. Use the retrieval path approved for your product surface; do not assume a file read helper exists unless your SDK version exposes one.
Start from Files
Use files when your app wants to hand Coder a small repro, generated fixture, or one-off workspace. Keep the task prompt focused on the output contract. The session can stay live after the task finishes, so the contract is how your app decides whether the work is done.
Do not set runtime for the normal managed-session path. Coder chooses a runtime that includes the Hub driver. Reach for runtime only when you are intentionally validating an advanced Coder runtime configuration.
import { Buffer } from 'node:buffer';
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
async function main(): Promise<void> {
const client = new CoderClient();
const runId = `calc-fix-${Date.now()}`;
const resultPath = `agent-output/${runId}/result.json`;
const reportPath = `agent-output/${runId}/report.md`;
const created = await client.createSession({
label: `Fix total() ${runId}`,
workflowMode: 'standard',
defaultAgent: 'builder',
enabledAgents: ['builder', 'reviewer'],
tags: ['docs-example', runId],
metadata: {
runId,
resultPath,
reportPath,
},
files: [
{
path: 'package.json',
content: Buffer.from(
JSON.stringify(
{
type: 'module',
scripts: { test: 'bun test' },
devDependencies: { '@types/bun': 'latest' },
},
null,
2
)
),
},
{
path: 'src/calc.ts',
content: Buffer.from(
[
'export function total(values: readonly number[]): number {',
' return values.reduce((sum, value) => sum - value, 0);',
'}',
'',
].join('\n')
),
},
{
path: 'src/calc.test.ts',
content: Buffer.from(
[
"import { expect, test } from 'bun:test';",
"import { total } from './calc.ts';",
'',
"test('totals positive values', () => {",
' expect(total([2, 3, 5])).toBe(10);',
'});',
'',
].join('\n')
),
},
],
task: [
'Goal: fix the TypeScript utility in this session workspace.',
'',
'Inputs:',
'- src/calc.ts currently implements total incorrectly.',
'- src/calc.test.ts is the verification command target.',
'',
'Do:',
'- inspect the provided files',
'- fix src/calc.ts',
'- run bun test',
`- write ${resultPath}`,
`- write ${reportPath}`,
'',
'Result JSON shape:',
'{',
' "status": "completed" | "blocked",',
' "changedFiles": string[],',
' "commandsRun": string[],',
' "verification": { "command": string, "passed": boolean, "outputExcerpt": string },',
' "blockers": string[]',
'}',
'',
'Stop when bun test passes and the two output files are written, or when a blocker is proven with exact command output.',
].join('\n'),
});
logger.info('coder session started', {
sessionId: created.sessionId,
resultPath,
reportPath,
});
}
void main();Example log shape:
{
"sessionId": "codesess_23bc720766d7",
"resultPath": "agent-output/calc-fix-123/result.json",
"reportPath": "agent-output/calc-fix-123/report.md"
}The two agent-selection fields do different jobs:
enabledAgentschooses the specialist roster for this session.defaultAgentchooses the default route target when work is not explicitly routed elsewhere.- Omit both fields when you want the Hub's normal Coder team behavior.
After creation, store sessionId, resultPath, and reportPath with your app record. Then use the read APIs below from a UI refresh, background job, webhook flow, or scheduler.
const [session, history] = await Promise.all([
client.getSession(sessionId),
client.listEventHistory(sessionId, { limit: 50 }),
]);
const taskCompleted = history.events.some((event) => event.event === 'task_complete');
logger.info('coder session progress', {
sessionId: session.sessionId,
status: session.status,
bucket: session.bucket,
runtimeAvailable: session.runtimeAvailable,
taskCompleted,
});In that shape, status: "active" does not mean the task is unfinished. A session can remain live for attach or follow-up work after the task completion event and output contract are satisfied.
Reading Session State
Once you have a session ID, use the read APIs to build dashboards, admin tools, or a simple status view in another app.
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
const sessionId = process.argv[2];
if (!sessionId) {
throw new Error('Pass a session ID as the first argument, for example: bun example.ts codesess_123');
}
async function main(): Promise<void> {
const client = new CoderClient();
try {
// Start with session detail; it's the fullest single view
const session = await client.getSession(sessionId);
// Pull related data in parallel when you need more than session detail alone
const [participants, history, replay] = await Promise.all([
client.listParticipants(sessionId, { limit: 20 }),
client.listEventHistory(sessionId, { limit: 20 }),
client.getReplay(sessionId, { limit: 50 }),
]);
logger.info(
JSON.stringify(
{
session: {
sessionId: session.sessionId,
label: session.label,
status: session.status,
bucket: session.bucket,
workflowMode: session.workflowMode,
historyOnly: session.historyOnly,
runtimeAvailable: session.runtimeAvailable,
controlAvailable: session.controlAvailable,
},
participantsCount: participants.participants.length,
historyCount: history.events.length,
replaySessionId: replay.sessionId,
},
null,
2
)
);
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
logger.error('Failed to read session state');
logger.error(message);
process.exitCode = 1;
}
}
void main();Fresh or idle sessions can legitimately return empty participants, event history, or replay data. That usually means the session has not produced much activity yet.
getReplay() returns the durable event stream a late-joining client uses to catch up to the Hub's current state. Treat the result as the hydration payload, not as a list you enumerate in a dashboard. For a structured timeline, use listEventHistory() instead.
Listing Sessions for Dashboards or History Views
If your app needs a broader session list before it drills into one session, start with listSessions().
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
async function main(): Promise<void> {
const client = new CoderClient();
try {
const { sessions, total } = await client.listSessions({
limit: 20,
includeArchived: true,
});
logger.info(
JSON.stringify(
{
total,
sessions: sessions.map((session) => ({
sessionId: session.sessionId,
label: session.label,
status: session.status,
bucket: session.bucket,
workflowMode: session.workflowMode,
historyOnly: session.historyOnly,
})),
},
null,
2
)
);
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
logger.error('Failed to list sessions');
logger.error(message);
process.exitCode = 1;
}
}
void main();Use listSessions() when you are building a dashboard or history view. Once the user picks a session, switch back to getSession(), replay, participants, or lifecycle methods. List items include status, bucket, workflow mode, and tags, but not the task prompt; that only appears on the full getSession() response. total can exceed sessions.length because the Hub counts a broader set than the SDK surfaces; render from sessions and treat total as a hint that more exist.
Pinning a GitHub Repo
When your app needs Coder to operate on a specific repository, use listGitHubAccounts() to retrieve the GitHub accounts connected via the GitHub App installation, then listGitHubRepos(accountId) to list repositories under that account. Pass the chosen repository to createSession() via the repo field.
listGitHubAccounts() reads the GitHub App installations visible to the API key's org. If the call fails or returns no accounts, check the Agentuity Console, confirm the GitHub App is installed for the right org, and then retry.
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
async function main(): Promise<void> {
const client = new CoderClient();
let github;
try {
// This depends on the GitHub App installations visible to the current org
github = await client.listGitHubAccounts();
} catch {
throw new Error(
'Check your Agentuity org and GitHub App installation, then run this again to list the GitHub accounts available to your org.'
);
}
const { connected, accounts } = github;
if (!connected) {
throw new Error('Connect GitHub in the Agentuity Console first.');
}
if (accounts.length === 0) {
throw new Error('No GitHub accounts connected. Install the Agentuity GitHub App first.');
}
// Pick the first account, or match by accountName in your own logic
const account = accounts[0];
if (!account) {
throw new Error('No GitHub account selected.');
}
// List repos visible under that account
const { repositories } = await client.listGitHubRepos(account.accountId);
if (repositories.length === 0) {
throw new Error(`No repositories found under account ${account.accountName}`);
}
// Pick a repository; this example just takes the first
const repo = repositories[0];
if (!repo) {
throw new Error('No repository selected.');
}
// Create the session with the pinned repository reference
// CoderSessionRepositoryRef accepts owner, name, and optionally branch
const created = await client.createSession({
task: 'Review the open pull requests and summarize any security-related changes.',
workflowMode: 'standard',
repo: {
owner: repo.owner.login,
name: repo.name,
// branch: 'main', // optional: pin to a specific branch
},
});
logger.info('Session:', created.sessionId, 'pinned to repo:', repo.fullName);
}
void main();listGitHubAccounts() reflects the GitHub App installations active for the API key's org. If accounts is empty, install the Agentuity GitHub App from your org settings in the Agentuity Console and grant it access to the repositories you need, then try again.
Updating and Cleaning Up
Use updateSession() to relabel a session, retag it, adjust its visibility, or update its metadata. Then archive the session for later replay, or delete it.
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
const sessionId = process.argv[2];
const action = process.argv[3] ?? 'archive';
if (!sessionId) {
throw new Error(
'Pass a session ID as the first argument, for example: bun example.ts codesess_123 archive'
);
}
async function main(): Promise<void> {
const client = new CoderClient();
try {
// updateSession is for app-level session detail, not runtime control
await client.updateSession(sessionId, {
tags: ['docs', 'follow-up'],
visibility: 'organization',
// metadata carries app-owned session context for later lookup
metadata: {
reviewStage: 'follow-up',
},
});
if (action === 'delete') {
// Delete temporary sessions when you don't need replay later
await client.deleteSession(sessionId);
logger.info(`Deleted ${sessionId}`);
return;
}
// Archive by default so session history remains available for replay
const archived = await client.archiveSession(sessionId);
logger.info(JSON.stringify({ sessionId, status: archived.status }, null, 2));
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
logger.error('Failed to manage the session');
logger.error(message);
process.exitCode = 1;
}
}
void main();For quick local validation, deleting the session is fine:
await client.deleteSession(sessionId);Archive the session instead when you want to inspect its history later:
await client.archiveSession(sessionId);Key Points
- Keep the
sessionIdfromcreateSession(). Most follow-on APIs start there. - In app code, pair
taskwithlabelfor list UIs,metadatafor correlation ids, andenvfor credentials the sandbox-side agent reads. - Put output paths, run ids, and expected artifacts in the task and metadata before the session starts.
- Treat
createSession()as the beginning of long-running work, not a synchronous result. - Use
listSessions()for dashboards and history views, thengetSession()for the chosen entry. - Use
archiveSession()for sessions you may want to inspect later, anddeleteSession()for temporary tests and cleanup. - To reconnect to an older session, switch to the reconnect flow instead of starting from
listSessions().
See Also
- Reconnecting to Existing Coder Sessions: Find live or wakeable sessions before re-attaching
- Creating Loop-Mode Coder Sessions: Use loop workflow state instead of a standard session
- Coder: Concepts, workflow modes, and CLI entry points
- Coder Client: Full
CoderClientmethod reference - Coder Commands: CLI flows for creating and attaching to sessions