Agents
First-class actor agents in standards — roles, tools, audit trail, and trigger mechanisms.
Agents are first-class actors that can read, write, and act on records using LLMs. Each agent has its own roles, tools, and audit trail — they are indistinguishable from users in the permission and audit systems.
Define an agent
// @noverify
import { useCreateAgentDefinition } from "@stndrds/react";
const create = useCreateAgentDefinition();
create.mutate({
name: "support-agent",
systemPrompt: "You triage incoming support requests...",
model: { provider: "anthropic", model: "claude-sonnet-4-5" },
tools: ["search_contacts", "create_note"],
});CreateAgentDefinitionInput requires name, systemPrompt, and model. tools is an optional array of tool names registered in the schema. Other optional fields include description, maxIterations, canDelegate, maxDepth, maxTreeCostUsd, and schedule.
Agent identity
Each agent_definitions row triggers creation of an actor row (type='agent') via a SQL trigger. The agent shows up everywhere actors do — in audit logs, in role assignments via useActorRoles(actorId), and in the actor picker. Assign roles to an agent the same way you would to a user.
Audit delegation chain
When an agent acts on behalf of a user, AsyncLocalStorage carries three fields:
actorId— who is executing right now (the agent or the user)triggeredByActorId— the actor that started this steprootActorId— the originating human user at the top of the chain
This lets you write audit queries like "show everything Claude did for Alice yesterday" by filtering on rootActorId = alice and actorId = agent.
Tools
Tools are typed functions registered in the schema that agents can call. The agent runtime validates inputs and routes calls through standards' adapters. A tool named "search_contacts" maps to a resolver registered in the backend — no ad-hoc function passing.
Each agent response is capped at a default output-token budget (32 000 tokens, clamped to the model's declared limit), so a single tool call can't inline an unbounded payload. For large text content (more than a few KB), the agent writes the content to a sandbox file and passes { "$fromSandboxFile": "/workspace/<file>" } as the attribute value in create_record / update_record — the server substitutes the file content before writing. This reference form is not supported in bulk tools. When a tool part is cut short, it now ends with a structured errorCode ("output_limit_reached" or "interrupted") so the chat UI can render a localized explanation instead of a raw error.
Built-in skills
@stndrds/runtime ships BUILT_IN_SKILLS that are loaded into the agent system prompt on demand: document-generation (DOCX/PDF/PPTX/XLSX via LibreOffice and the docx npm package), browser-automation (drive Chromium with playwright-cli inside the sandbox to inspect UI state, capture screenshots, and debug console/network failures), and ui (product-language tour of the app, kept jargon-free for agents talking to end-users). The E2B sandbox template now installs playwright@1.60.0 and pre-fetches Chromium so the browser-automation skill works out of the box.
Skills are now also records. When agents are enabled, the runtime registers a sealed skill object so a tenant can author its own skills at runtime — each record carries name, displayName, description, content (richtext), optional attachments, plus alwaysOn and enabled flags. Tenant skill records are merged with BUILT_IN_SKILLS: a record sharing a builtin's name overrides it. A skill with alwaysOn has its description injected into the system prompt; the rest stay discoverable through the find_skill tool and are pulled in on demand via get_skill. Set enabled: false to exclude a skill from both the prompt and discovery.
Environment variables
Agents can read tenant-scoped or per-agent environment variables at runtime. Values are stored encrypted (pgcrypto) and resolved on each execution — only metadata is ever returned to the client. The resolved keys are injected into the agent sandbox and appended to the system prompt as an ## Available Environment Variables section so the model knows which $KEY_NAME references are valid in shell tool calls.
Per-agent values override tenant-level values for the same key. Archiving an agent deletes its scoped variables.
Manage them from React via the env var hooks:
// @noverify
import { useAgentEnvVars, useUpsertAgentEnvVar, useDeleteEnvVar } from "@stndrds/react";
function AgentEnvVars({ agentId }: { agentId: string }) {
const { data: entries = [] } = useAgentEnvVars(agentId);
const upsert = useUpsertAgentEnvVar(agentId);
const remove = useDeleteEnvVar({ agentId });
return (
<div>
{entries.map((e) => (
<div key={e.id}>
{e.key} <button onClick={() => remove.mutate(e.id)}>delete</button>
</div>
))}
<button onClick={() => upsert.mutate({ key: "STRIPE_KEY", value: "sk_test_..." })}>
add
</button>
</div>
);
}Tenant-wide vars use useEnvVars / useUpsertEnvVar instead. The settings UI exposes WorkspaceEnvVarsSection for the tenant scope and an env vars tab on AgentSettingsDialog for the per-agent scope. Read and write require the env-vars system permission.
AI provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY) are still read from the server's process.env — they are not managed through this UI.
Sub-agents spawned via dispatch_agent inherit env vars from their parent chain. A dispatched session starts without its own definition, so the runtime walks up parentSessionId and re-resolves each ancestor's vars from the encrypted store (merged root → parent, nearer ancestor wins), then layers the sub-agent's own vars on top. Plaintext secrets are never persisted on the sub-session.
Tenant system prompt
A tenant can define one system prompt that is prepended to every agent's own systemPrompt for that workspace — use it for workspace-wide tone, policy, or context that every agent should share. When no tenant prompt is set, the runtime falls back to the static config prefix, and a failed settings read never blocks execution.
Manage it from React with useTenantSettings / useUpdateTenantSettings:
// @noverify
import { useTenantSettings, useUpdateTenantSettings } from "@stndrds/react";
function WorkspacePrompt() {
const { data: settings } = useTenantSettings();
const update = useUpdateTenantSettings();
return (
<textarea
defaultValue={settings?.systemPrompt ?? ""}
onBlur={(e) => update.mutate({ systemPrompt: e.target.value || null })}
/>
);
}The settings UI exposes WorkspaceSystemPromptSection (from @stndrds/ui/settings-dialog) for this tenant scope. Reading the prompt requires the workspace read permission and saving requires workspace update.
useUpdateTenantSettings follows PATCH semantics: a field you omit is left
unchanged, so an empty or partial update never wipes a saved prompt. Pass an
explicit systemPrompt: null to clear it.
Trigger an agent
- From a UI button — call
useCreateAgentSession(definitionId)and.mutate({ input })on user action. - From a webhook — the API exposes a session-start endpoint; POST to it with an API key.
- From a record event — use agent triggers (
useCreateAgentTrigger) to fire automatically when a record changes. - On a schedule — set a cron expression on the agent definition; the
BullMQScheduleAdapterenqueues ascheduled-jobpayload that the runtime translates into alaunchRuncall.
Background agents are opt-in. The agents block in SchemaModule.forRoot({ agents: { enabled: true, ... } }) must be set explicitly, and the BullMQ/Redis adapters must be wired — otherwise the module is disabled to avoid a startup crash when Redis is not configured.
Agent runs are now persisted in the background_jobs ledger before they are dispatched to BullMQ. If a worker crashes mid-run the job stays claimable and is re-published by the recovery service, so triggers and scheduled runs survive restarts. Each run also holds a time-boxed execution lease that it heartbeats while running: a crashed worker's run becomes re-acquirable once the lease expires, and the fencing token stops a resurrected worker from double-writing the same run.
Never set tool-call limits on spawned subagents — let them run to natural completion.
Agent behavior in some deployments is gated behind the agent_explicit_permissions feature flag. When disabled, agents inherit the permissions of the actor that triggered them.
Archiving an agent definition (soft-delete via archiveDefinition) disables all of its triggers so matching events stop spawning runs; restoreDefinition re-enables them. launchRun also refuses to start when the definition's deletedAt is set, so an archived agent is effectively paused end-to-end.