Skip to content

Reference

Agents & Keys

Managed agents (app.agents namespace), Agent runtime (me/trace/withConstraints), scoped key lifecycle (keys namespace).

This page covers the full lifecycle of managed agents and the scoped-key primitives they’re built on:

// Operator side
const created = await app.agents.create({
name: "research-agent",
scopes: { "<provider-id>": ["scope.read", "scope.write"] },
});
const page = await app.agents.list();
const info = await app.agents.get(agentId);
await app.agents.update(agentId, { displayName: "Research Agent v2" });
await app.agents.suspend(agentId);
await app.agents.resume(agentId);
await app.agents.delete(agentId);
// Workload side
const me = await agent.me();
await agent.trace({ runId: "run_42" }, async () => {
await agent.request(...);
});
const narrowed = agent.withConstraints({ scopes: ["grants:read"] });
const session = await agent.createManagedSecretConnectSession({
templateSlug: "provider-api-key",
userToken: userJwt,
});
// Scoped key lifecycle
const derived = await app.keys.derive({ scopes: ["tokens:retrieve"], expiresIn: 3600 });
const rotated = await app.keys.rotate({ keyId: old.id });
const revoked = await app.keys.revoke({ keyId: leaked.id });

import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });
try {
const created = await app.agents.create({
name: "research-agent",
scopes: { "<provider-id>": ["scope.read", "scope.write"] },
});
console.log(created.apiKey); // shown ONCE
} finally {
await app.close();
}

Provision a managed agent and mint its initial API key.

async create(options: AgentCreateOptions): Promise<AgentCreateResult>
OptionTypeDescription
namestringStable per-app identifier. Required.
displayNamestringHuman-readable label.
type"agent" | "service"Defaults to "agent".
scopesRecord<string, string[]>Required per-provider capability allowlist, maximum 12 KiB as compact UTF-8 JSON. Each listed provider must contain at least one scope; an empty object ({}) provisions a deny-all agent with no provider access until scopes are added. Managed-secret capabilities go under managed_secret_slugs as each secret’s stable, app-unique slug (a display-name rename keeps the permission). The legacy managed_secrets key matches display names and accepts no new entries.
metadataRecord<string, unknown>Free-form metadata (≤8 KB).
policyRecord<string, unknown>HITL config + policy attributes.
idempotencyKeystringCaller-supplied idempotency key. No CR/LF.

Returns AgentCreateResult. The plaintext apiKey is returned exactly once — store it securely on receipt. On an idempotency replay where the original key cannot be re-issued, apiKey is null; branch on result.apiKey === null to detect replay.

Throws AlterValueError, AgentNameExistsError, AgentCannotMintSubagentsError, IdempotencyKeyBodyMismatchError, IdempotencyKeyAgentRevokedError, IdempotencyKeyAgentInactiveError, and BackendError with HTTP 422 and details.error === "legacy_managed_secret_name_rejected" when scopes lists any entry under the legacy managed_secrets key (nothing is created).

Paginated list of agents in the app.

async list(options?: AgentListOptions): Promise<AgentListResult>
OptionTypeDefaultDescription
includeRevokedbooleanfalseInclude tombstoned agents.
limitnumber100Requested page size (1..1000). The server returns at most 10 agents per page.
offsetnumber0Page offset.

Returns AgentListResult.

Fetch a single agent by UUID.

async get(agentId: string): Promise<AgentInfo>

Throws AgentNotFoundError.

Fetch an active agent by exact name. Returns null when no matching agent exists — doesn’t throw on the common “does it exist?” check.

async getByName(name: string): Promise<AgentInfo | null>
const info = await app.agents.getByName("research-agent");
if (info === null) {
throw new Error("Agent not provisioned");
}
const agent = app.getAgent(info.id);

Update label or scope/policy block on an existing agent.

async update(
agentId: string,
options?: AgentUpdateOptions,
): Promise<AgentInfo>
OptionTypeDescription
displayNamestringNew display name.
metadataRecord<string, unknown>Replacement metadata (pass {} to clear).
scopesRecord<string, string[]>Full desired per-provider allowlist, maximum 12 KiB as compact UTF-8 JSON. Requires expectedVersion. Broadening applies immediately; narrowing is staged. Managed secrets go under managed_secret_slugs by slug; entries the agent already stores under the legacy managed_secrets key may be kept or removed, never added.
expectedVersionnumberPositive AgentInfo.version from the snapshot used to build scopes. Required whenever scopes is supplied.
graceSecondsnumberNarrowing grace, 0–604800. Requires scopes; 0 applies immediately. Omit for the 24-hour deployment default.
policyRecord<string, unknown>Replacement policy block.

Passing no options is a no-op round-trip. Returns the updated AgentInfo.

const snapshot = await app.agents.get(agentId);
await app.agents.update(agentId, {
scopes: { "<provider-id>": ["scope.read"] },
expectedVersion: snapshot.version,
graceSeconds: 3600,
});

If another operator changes the agent after snapshot was read, the backend throws AgentConcurrentUpdateError. Fetch the agent again, recompute the full desired allowlist from that fresh record, and retry with its version. Never blindly replay scopes built from the stale snapshot: doing so could restore access the other operator removed.

Adding a name under the legacy managed_secrets key throws BackendError with HTTP 422 and details.error === "legacy_managed_secret_name_rejected"; nothing is saved. List the secret’s slug under managed_secret_slugs instead.

Pause and later restore an agent without revoking its identity.

async suspend(agentId: string): Promise<AgentInfo>
async resume(agentId: string): Promise<AgentInfo>
  • suspend() flips status to "inactive". Every call made with that agent’s key — agent.request(), agent.proxyRequest(), agent.listGrants(), agent.delegate(), Connect session minting, key derivation, catalog reads — then throws AgentInactiveError: suspension is containment, so a paused agent can neither enumerate its grants nor acquire new access. agent.me() keeps working so a paused workload can self-diagnose, and telemetry ingestion (span emission and the SDK’s automatic provider-call audit reporting) keeps flowing so pausing an agent never blinds its audit trail.
  • resume() flips status back to "active". Any overdue pending scope narrowing stays on the row and applies on the next auth check or sweeper pass.
await app.agents.suspend(agentId);
await app.agents.resume(agentId);

Hard-revoke an agent and cascade-revoke every API key under it.

async delete(agentId: string): Promise<AgentInfo>

Idempotent — calling delete() on an already-revoked agent returns the same row.

Each managed agent can hold multiple API keys. Multiple keys may remain active during a rotation window; the last-active-key guard prevents accidental lockout. Use the rotation surface for credential rolls without bricking the workload.

async mintKey(agentId: string): Promise<AgentKeyMintResult>
async listKeys(agentId: string): Promise<AgentKeyList>
async deprecateKey(agentId: string, keyId: string): Promise<AgentKey>
async undeprecateKey(agentId: string, keyId: string): Promise<AgentKey>
async revokeKey(
agentId: string,
keyId: string,
options?: { force?: boolean },
): Promise<AgentKey>
  • mintKey — mint a successor key. The plaintext is returned once.
  • listKeys — return every key for the agent (active, deprecated, revoked).
  • deprecateKey — keep the key working but mark it as the old key. Every authenticated response carries X-Alter-Key-Deprecated: true, and a client using the key emits a one-time warning through the SDK logger (console.warn by default; route or silence it via the logger option) so operations dashboards spot “time to roll the workload.”
  • undeprecateKey — clear the deprecation marker.
  • revokeKey — terminal revoke. Default-protective: refuses to revoke the agent’s last active key. Pass force: true to override.

Throws KeyNotFoundError, KeyAlreadyRevokedError, LastActiveKeyError, and — from mintKey, when the agent is at its active-key cap — AgentKeyLimitError (revoke an existing key to free a slot).


Return the calling agent’s own record.

async me(): Promise<AgentInfo>

Calling me() through an impersonating Agent returned by App.getAgent() uses the application’s key and throws MeRequiresAgentKeyError from the backend. The check is enforced server-side; the carve-out lets paused or revoked agents with genuine agent keys call me() for self-diagnosis (but a revoked individual key still throws KeyRevokedError).

const agent = new Agent({ apiKey: process.env.AGENT_API_KEY! });
try {
const info = await agent.me();
console.log(info.status, info.scopes);
} finally {
await agent.close();
}

Run a callback inside an audit-context scope. Every nested SDK call propagates the agent identity, run ID, and optional metadata for audit attribution.

async trace<T>(
options: {
runId?: string;
threadId?: string;
parent?: string | null;
[metadataKey: string]: unknown;
},
callback: () => Promise<T>,
): Promise<T>
OptionTypeDescription
runIdstringStable per-run identifier (e.g. LangChain run_id). Threads through to the audit row.
threadIdstringStable per-conversation identifier.
parentstring | nullExplicit parent identity. Overrides ambient detection.
[metadataKey]stringFree-form metadata. Values must be strings.

Inside the callback, every request(), proxyRequest(), or nested trace() on this agent — or on any other client constructed in the same async context — carries the scope automatically.

await agent.trace({ runId: "run_abc", role: "writer" }, async () => {
await agent.request("GET", url, { grantId });
await researcher.trace({}, async () => {
// parent_agent populated automatically via ambient detection.
await researcher.request(...);
});
});

When an outer trace() of a different agent is active, parent_agent is recorded automatically. Re-entering trace() for the same agent does not set parent_agent.

These keys are reserved and throw AlterValueError when passed via metadata:

  • agent
  • parent_agent
  • run_id (use the explicit runId option instead)
  • thread_id (use the explicit threadId option instead)
  • tool
  • tool_call_id
  • framework

Return a constrained sibling Agent. Pass scopes to narrow the scope set, rule to attach a request rule, or both; at least one is required. Calling it on a closed client throws AlterSDKError. See withConstraints on the Client page for the full surface, including the RequestRule shape.

const readOnly = agent.withConstraints({ scopes: ["grants:read"] });
const page = await readOnly.listGrants();
// Attach a request rule:
const restricted = agent.withConstraints({
rule: {
ruleType: "json_match",
ruleBody: { when: { method: "POST" }, effect: "deny" },
},
});

The constrained sibling preserves the parent agent’s trace() identity — the run ID, thread ID, and metadata propagate through the scope-narrowed client.

Ask a user to delegate their managed-secret access to this agent. It is the agent-side form of App.createManagedSecretConnectSession(): the same session, the same sign-in and approval page, the same outcome delivery and the same return value, with no target option.

async createManagedSecretConnectSession(
options: AgentCreateManagedSecretConnectSessionOptions,
): Promise<ManagedSecretConnectSession>

AgentCreateManagedSecretConnectSessionOptions is exported from @alter-ai/alter-sdk. It is every CreateManagedSecretConnectSessionOptions field except delegatedAgentId:

OptionTypeDescription
templateSlugstringCanonical template slug in kebab-case. Required.
userTokenstringIDP JWT identifying the user being asked. Required.
requestedTtlSecondsnumberCaller-suggested TTL, capped as on the App method.
allowedOriginstringPopup-flow postMessage origin: a single canonical origin, as on the App method (otherwise HTTP 400 invalid_origin).
returnUrlstringMobile-redirect return URL.
allowUserPolicyRulesbooleanWhether the user may add narrowing-only usage limits during approval. Default true.
delegablebooleanAsk the user to let this agent share the access onward; the Connect page says next to its checkbox who asked: the agent itself for its own key, the application for an Agent from App.getAgent(). For a new delegation the checkbox starts off when the agent’s own key asked, so the user opts the agent in, and on for an Agent from App.getAgent(), whose request comes from the application; an existing delegation starts from its stored setting. Default false. Echoed as delegableRequested. See the App method.

Which agent the session is for. The session is always for the agent this client acts as:

  • An Agent built from the agent’s own key (new Agent({ apiKey })) sends no agent id; the backend targets the key’s agent.
  • An Agent from App.getAgent() sends its agent id under the application’s key, so the request is exactly app.createManagedSecretConnectSession({ delegatedAgentId }) for that agent.
  • An Agent constructed directly from an application key, or from an agent-typed alter_ak_… key minted under API Keys (bound to no managed agent), is neither, so it names no agent; the backend refuses the request with HTTP 422 delegated_agent_id_required (BackendError). Use App.getAgent() with an application key, or the managed agent’s own key (alter agents mint-key).

Only an Agent from App.getAgent() receives no_eligible_grants. It uses the application’s key, so like the App method it throws ConnectConfigError with no_eligible_grants when the user holds nothing that could be delegated to this agent. An Agent built directly from a key bound to no managed agent is refused first with delegated_agent_id_required (above). A managed agent’s own key never receives it: session creation does not look at the user’s credentials at all, and a created session has the same fields as one created with an app key, so session creation tells an agent nothing about which credentials a user holds. If the user has nothing to delegate, the Connect page tells the user why after they sign in.

Scopes. The calling key needs connect:initiate and secrets:write, the same pair an app key needs for the App method. Managed-agent keys carry both.

Returns ManagedSecretConnectSession. On approval the page delivers the new delegation_id (with updated and delegable), which the agent passes as grantId to proxyRequest().

Throws:

  • AlterValueError — a missing or malformed templateSlug or userToken, or a non-boolean allowUserPolicyRules / delegable, before any request.
  • ConnectConfigError — idp_signin_not_configured; no_eligible_grants from an App.getAgent() client only.
  • ServerFeatureUnsupportedError — the backend predates this method, or ignored delegable: true.
  • AgentInactiveError — the agent is paused.
  • AgentNotFoundError — from an App.getAgent() client whose agent is no longer an active agent of the app (details.error === "caller_agent_unresolved").
  • BackendError — invalid_origin, delegated_agent_id_required, an unknown template, a user token that fails validation, or a user who cannot be synchronized (statusCode 503 service_unavailable, transient, with details.retry_after_seconds, or 409 identity_provider_inactive / identity_group_name_ambiguous, which an administrator must repair).
import { Agent, ServerFeatureUnsupportedError } from "@alter-ai/alter-sdk";
const agent = new Agent({ apiKey: process.env.AGENT_API_KEY! });
try {
const session = await agent.createManagedSecretConnectSession({
templateSlug: "provider-api-key",
userToken: process.env.USER_JWT!, // the end user the agent is acting for
});
// Send the user to session.connectUrl; after approval, the delegation id
// arrives in the postMessage payload or on returnUrl.
console.log(session.connectUrl);
} catch (error) {
if (error instanceof ServerFeatureUnsupportedError) {
// The Alter backend predates agent-initiated requests: ask from the
// application (app.createManagedSecretConnectSession) instead.
}
throw error;
} finally {
await agent.close();
}

The keys namespace lifecycle-manages scoped API keys. Available on both App and Agent — agent callers typically only have permission to call derive().

import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey });
try {
const derived = await app.keys.derive({
scopes: ["tokens:retrieve", "grants:read"],
expiresIn: 3600,
cidrAllowlist: ["10.0.0.0/8"],
metadata: { purpose: "ci-deploy" },
});
process.env.CHILD_KEY = derived.apiKey; // shown ONCE
} finally {
await app.close();
}

Mint a short-lived attenuated key under the calling key.

async derive(options: DeriveKeyOptions): Promise<MintedKey>
OptionTypeDescription
scopesreadonly string[]Scopes the derived key holds. Must be a non-empty subset of the caller’s effective scopes. keys:derive is rejected — derived keys cannot themselves derive.
expiresInnumberLifetime in seconds. Capped by org policy (maxDerivedKeyTtlHours, default 24 h) and by the caller’s remaining TTL.
cidrAllowlistreadonly string[]Optional IP allowlist. Must be a subset of the caller’s allowlist.
namestringOptional label.
metadataRecord<string, unknown>Free-form metadata stored on the key.

Returns MintedKey — extends APIKeyInfo with the plaintext apiKey. The plaintext is returned exactly once. Store it securely.

Throws:

Rotate an existing key: mint a same-scope successor with an overlap window where both keys work.

async rotate(options: RotateKeyOptions): Promise<MintedKey>
OptionTypeDefaultDescription
keyIdstring | { toString(): string }—The key to rotate. UUID strings and UUID-like objects are accepted. Required.
overlapDaysnumber7Days the old key remains valid. Range 0..30.

Returns MintedKey — the successor’s plaintext is on apiKey, returned once.

Derived (dk) keys cannot be rotated. Derive a new sub-key from the parent instead.

A managed-agent key can rotate and revoke only keys bound to its own agent; any other key in the application is reported as not found. An application key can manage every key in its application.

Throws:

Revoke a key and cascade-revoke every derived (dk) descendant.

async revoke(options: RevokeKeyOptions): Promise<APIKeyInfo>
OptionTypeDefaultDescription
keyIdstring | { toString(): string }—The key to revoke. UUID strings and UUID-like objects are accepted. Required.
forcebooleanfalseBypass the last-active-key guard.

Default-protective: refuses to revoke the last active key on a managed agent. Pass force: true to override (e.g. incident response on a leaked key without a successor).

Returns the post-revoke APIKeyInfo.

Throws:

Synchronous, no-network complete format check for a plaintext key. Scoped keys re-derive and compare the embedded CRC; legacy keys enforce their exact mint-time length and alphabet.

import { isValidKey } from "@alter-ai/alter-sdk";
isValidKey("alter_rk_…"); // true | false
isValidKey(undefined); // false
function isValidKey(plainKey: unknown): boolean

Returns false for non-strings, empty strings, unknown key-type discriminators, the wrong segment count, wrong alphabet or length, or a mismatched checksum. Legacy keys must be alter_key_ plus exactly 40 ASCII letters, digits, -, or _. Scoped keys require a supported type, exactly 22 lowercase-base32 random characters, and a matching 6-character lowercase-base32 CRC. Does not throw.

This is a usability gate that catches single-character typos before paying a network round-trip. It is not a security boundary — a forged checksum cannot pass backend authentication because the server verifies the key against its hashed fingerprint.

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.