The @getstackrun/sdk package is the STACK client for Node.js JavaScript and TypeScript applications. The package requires Node.js 18 or newer.
npm install @getstackrun/sdkSign in once on your development machine:
npx @getstackrun/cli auth loginimport { Stack } from '@getstackrun/sdk';
const stack = new Stack();
const agents = await stack.agents.list();
// Open a Mission for one agent and issue its Passport in one call.
const started = await stack.missions.start({
agent_id: 'agt_support',
mission: { intent_summary: 'Post the daily support update', intent_services: ['slack'] },
});
const token = started.passport.token;
// Completing the Mission revokes its Passports.
await stack.missions.complete(started.mission_id);new Stack() creates an API client and can use the local OAuth profile written by the CLI. A fleet worker uses new Stack({ agent_id }) to sign requests with its locally stored key.
Every Passport is issued for exactly one Mission. stack.missions.start opens the Mission and issues its Passport in one call. If issuance fails, the Mission is revoked.
Connect the service first. The policy must name an enrolled provisioner and the connected service. In this example, waitForApproval opens the returned approval URL and waits for the operator to approve the policy. It runs once for policy activation. createWorker returns new Stack({ agent_id: agentId }). The worker can then start Missions within the approved policy.
import type { Stack, FleetPolicyInput } from '@getstackrun/sdk';
export async function setupFleet(options: {
operator: Stack;
provisioner: Stack;
policy: FleetPolicyInput;
policyKey: string;
workerName: string;
enrollmentKey: string;
createWorker: (agentId: string) => Stack;
waitForApproval: (approval: { approval_id: string; approval_url: string }) => Promise<void>;
}) {
let policy;
try {
policy = await options.operator.fleets.activate(options.policy, { idempotencyKey: options.policyKey });
} catch (error) {
const failure = error as { code?: string; details?: { error?: { details?: { approval_id?: string; approval_url?: string } } } };
const approval = failure.details?.error?.details;
if (failure.code !== 'GOVERNANCE_APPROVAL_REQUIRED' || !approval?.approval_id || !approval.approval_url) throw error;
await options.waitForApproval({ approval_id: approval.approval_id, approval_url: approval.approval_url });
policy = await options.operator.fleets.activate(options.policy, {
idempotencyKey: options.policyKey, governanceApprovalId: approval.approval_id,
});
}
const enrolled = await options.provisioner.fleets.enrollLocal(policy.id, {
name: options.workerName, idempotency_key: options.enrollmentKey,
});
const worker = options.createWorker(enrolled.agent_id);
const session = await worker.fleets.session();
return { policy_id: policy.id, agent_id: enrolled.agent_id, worker, session };
}
For workload enrollment, create a client with the trusted workload bearer token and call enrollWorkloadLocal. Use the returned agent ID with a worker client for later requests. Keep the workload token in the runtime's secret store.
enrollLocal and enrollWorkloadLocal store the private key on the worker's machine. Reusing the policy ID and enrollment key reuses that key and renews the same membership. Keep those values stable for that worker.
import assert from 'node:assert/strict';
import type { Stack, FleetMissionStartInput, ProxyRequest } from '@getstackrun/sdk';
export async function runFleetMissionExample(stack: Stack, request: FleetMissionStartInput, providerUrl = 'https://slack.com/api/chat.postMessage') {
const started = await stack.missions.start(request);
const repeated = await stack.missions.start(request);
assert.equal(repeated.mission_id, started.mission_id);
const call: ProxyRequest = { service: 'slack', method: 'POST', url: providerUrl,
body: { channel: 'C123', text: 'Support update' } };
const { createHash } = await import('node:crypto');
const operationId = createHash('sha256').update(JSON.stringify([started.mission_id, 'support-update'])).digest('base64url');
const response = await stack.proxy.request(call, { passportToken: started.passport.token, authorityRequestId: operationId });
assert.equal(response.status, 200);
await assert.rejects(() => stack.proxy.request({ ...call, body: { channel: 'OTHER', text: 'Support update' } },
{ passportToken: started.passport.token, authorityRequestId: `${operationId}_denied` }),
(error: { status?: number }) => error.status === 403);
const before = await stack.missions.get(started.mission_id);
const renewed = await stack.missions.renew(started.mission_id);
const after = await stack.missions.get(renewed.mission_id);
assert.equal(renewed.mission_id, started.mission_id);
assert.deepEqual(after.fleet?.limits, before.fleet?.limits);
assert.deepEqual(after.fleet?.usage, before.fleet?.usage);
const completed = await stack.missions.complete(started.mission_id, { output: { done: true } });
assert.equal(completed.terminal_state, 'completed_successful');
const stopped = await stack.missions.start({ ...request, idempotency_key: `${request.idempotency_key}-stop` });
await stack.missions.revoke(stopped.mission_id, { reason: 'operator_initiated_clean' });
await assert.rejects(() => stack.missions.renew(stopped.mission_id));
return { mission_id: started.mission_id, operation_id: operationId, completed: true, revoked: true };
}
stack.fleets.list()
stack.fleets.listPage(page, limit)
stack.fleets.get(policyId)
stack.fleets.membership(agentId)
stack.fleets.membersPage(policyId, page, limit)
stack.fleets.activate(policy, { idempotencyKey, governanceApprovalId })
stack.fleets.revise(policyId, policy, { expectedRevision, idempotencyKey, governanceApprovalId })
stack.fleets.members(policyId)
stack.fleets.revokeMember(policyId, agentId)
stack.fleets.revoke(policyId)
stack.fleets.enrollLocal(policyId, { name, idempotency_key })
stack.fleets.enrollWorkloadLocal(policyId, { name, idempotency_key })
stack.fleets.session(fleetPolicyId)
stack.missions.start({ agent_id, mission: { intent_summary, intent_services?, ... }, passport? })
stack.missions.start(fleetRequest) // fleet form: { fleet_policy_id, idempotency_key, ... }
stack.missions.open({ intent_summary, intent_services?, agent_id?, expires_in_seconds?, max_actions?, ... })
stack.missions.renew(missionId)
stack.missions.complete(missionId, { output })
stack.missions.revoke(missionId, { reason: 'operator_initiated_clean' })The operator form of missions.start returns { mission_id, claim_id, expires_at, passport: { token, jti, expires_at } }. passport is optional and takes the issue fields such as scopes, ttl_seconds and intent. missions.open assigns the Mission to agent_id when given. Completing or revoking a Mission revokes every Passport bound to it, for any revoke reason.
stack.agents.register({ name, description?, credential_access? })
stack.agents.list()
stack.agents.get(id)
stack.agents.update(id, { name?, status?, credential_access? })
stack.agents.delete(id)
stack.agents.unblock(id)stack.passports.issue({ agent_id, mission_ids: [missionId], scopes?, ttl_seconds?, authority_binding_ids?, ... })
stack.issuePassport({ agent_id, mission_ids: [missionId], ... }) // returns a PassportSession
stack.passports.verify(token, serviceId?)
stack.passports.revoke(jti, reason?)
stack.passports.delegate({ parent_token, child_agent_id, ... })
stack.passports.refresh(token, ttlSeconds?)
stack.passports.listActive({ agentId?, sessionId? })
stack.passports.revokeAgent(agentId, reason?)
stack.passports.revokeAll(reason?)
stack.passports.checkpoint(jti, input)
stack.passports.checkout(jti, input)mission_ids is required and holds exactly one live Mission ID. The Mission must be assigned to the agent or unassigned. The Passport never outlives its Mission. Refresh and delegation fail once the Mission is completed, revoked or expired, and a PassportSession from issuePassport stops refreshing with a clear error.
const mission = await stack.missions.open({
intent_summary: 'Post the daily support update',
intent_services: ['slack'],
agent_id: 'agt_support',
});
const passport = await stack.passports.issue({ agent_id: 'agt_support', mission_ids: [mission.mission_id] });authority_binding_ids attaches up to eight active bindings for the exact agent and current agent key. The returned Passport carries opaque authority_refs. Authority management and the native receiver protocol use the Authority REST API.
// Publishing
stack.skills.publish({ name, description, version, input_schema, output_schema, ... })
stack.skills.get(id)
stack.skills.browse({ query?, tags?, trust_level? })
stack.skills.listOwned()
stack.skills.update(id, updates)
stack.skills.suspend(id)
stack.skills.activate(id)
// Invocations
stack.skills.invoke(skillId, { agent_id?, input, passport_id? }, { approvalId?, authorityRequestId?, idempotencyKey? })
stack.skills.getInvocation(invocationId)
stack.skills.poll(invocationId, { intervalMs?, timeoutMs? })
stack.skills.listInvocations()
// Requests
stack.skills.postRequest({ description, max_price_cents?, tags? })
stack.skills.listRequests({ status?, limit? })
stack.skills.suggestComposition(requestId)stack.identity.listProviders()
stack.identity.initiateVerification({ provider_key, return_url? })
stack.identity.completeVerification(providerKey, sessionRef)
stack.identity.listClaims()
stack.identity.revokeClaim(id)
stack.identity.replaceServiceRequirement(serviceId, requirementOrNull)
stack.identity.grantDelegation(scopes)stack.services.listAvailable()
stack.services.listConnected()
stack.services.connect({ service_id, credential, scopes })
stack.services.connectCustom({ name, credential })
stack.services.disconnect(connectionId)
stack.credentials.get(provider)
stack.credentials.getByConnection(connectionId)
stack.dropoffs.create({ from_agent, to_agent, schema }, { passportToken? })
stack.dropoffs.deposit(id, agentId, payload, { passportToken?, passportJti?, missionId? })
stack.dropoffs.collect(id, agentId, { passportToken? })
stack.team.invite({ email, name, role?, allowed_connections? })
stack.team.list()
stack.team.update(id, { role?, allowed_connections? })
stack.team.revoke(id)
stack.deliveryMethods.add({ channel_type, destination })
stack.deliveryMethods.list()
stack.deliveryMethods.sendVerificationCode(id)
stack.deliveryMethods.verify(id, code)
stack.deliveryMethods.test(id)
stack.deliveryMethods.rotateSecret(id)
stack.deliveryMethods.delete(id)
stack.notifications.createRule({ destination_ids, events, min_severity? })
stack.notifications.listRules()
stack.notifications.updateRule(id, { destination_ids?, events?, min_severity? })
stack.notifications.deleteRule(id)
stack.securityEvents.list({ agentId?, page?, limit? })
stack.securityEvents.resolve(id)
stack.proxy.request(
{ service, method, url, headers?, body?, query? },
{ passportToken, approvalId?, authorityRequestId?, onOperationId? }
)
stack.proxy.getOperation(operationId, { passportToken, includeResponse? })
stack.scan.scan({ content, context?, source? }, { passportToken? })
stack.scan.usage()
stack.audit.list({ limit?, since?, agentId?, passportJti? })
stack.audit.chainHead()
stack.audit.verifyChain({ from?, to?, limit? })Non-GET proxy calls generate an operation ID before network I/O. Durable workers should use onOperationId to persist it, then reuse authorityRequestId only for the unchanged request. Use getOperation after response loss. See Proxy operation recovery.
// Tail recent entries — newest-first. Pass `since` (epoch ms) for
// incremental polling; pass `agentId` or `passportJti` to narrow.
const { entries, max_timestamp } = await stack.audit.list({
limit: 20,
agentId: 'agt_support',
});
// Anchor the chain head externally to prove no row was rewritten later.
const head = await stack.audit.chainHead();
console.log(head.latest_entry_hash, head.total_entries);
// Walk the chain and verify every entry's hash + link integrity.
const verdict = await stack.audit.verifyChain({ limit: 10_000 });
if (!verdict.valid) {
console.error('chain break at', verdict.first_break);
}Each entry carries layer, action,outcome, duration_ms, plus the hash-chain fields. Cascade revokes write onepassport.revoke entry plus onepassport.revoke_cascade per child — filter bypassportJti to see the chain for a single passport.
The SDK exports verifyPassportOffline as a standalone function (not a method on Stack). It verifies the JWT signature using only the public JWKS. It does not call the STACK API. Use it when a downstream service needs signature and expiry checks without an online verification call.
import { verifyPassportOffline } from '@getstackrun/sdk';
// Mode 1: JWKS URL - fetched + cached once per URL per process.
const claims = await verifyPassportOffline(token, {
jwksUrl: 'https://api.getstack.run/v1/.well-known/jwks.json',
});
// Mode 2: pre-fetched JWK - no network call at verification time.
const claims2 = await verifyPassportOffline(token, {
publicJwk: {
kty: 'OKP', crv: 'Ed25519', x: '...',
kid: 'stack-passport-v1', alg: 'EdDSA', use: 'sig',
},
});
console.log(claims.jti, claims.stk.agent_id, claims.stk.services);Offline mode checks the signature, issuer, audience, and token expiry when the token has anexp claim. Reject any result without exp. Offline verification does not check revocation or current account, agent, Mission, service, or identity state. Use POST /v1/passports/verify when current STACK state affects the decision.
Use this helper only with standard STACK-issued Passports until the SDK requires an expiry claim itself.
import { Stack, StackError, NotFoundError } from '@getstackrun/sdk';
try {
const agent = await stack.agents.get('agt_nonexistent');
} catch (err) {
if (err instanceof NotFoundError) {
console.log('Agent not found');
} else if (err instanceof StackError) {
console.log(err.code, err.status, err.message);
}
}Use the HTTP API directly when your integration needs an operation that this SDK does not expose. See /docs/api/billing for the billing shapes.
const headers = { Authorization: `Bearer ${process.env.STACK_API_KEY}` };
// Audit export (full date-range; the SDK's `stack.audit.list` covers tailing)
const exp = await fetch('https://api.getstack.run/v1/audit/export?format=ndjson', { headers });
// Billing balance
const balance = await fetch('https://api.getstack.run/v1/billing/balance', { headers }).then(r => r.json());