Skills and agents are currently in alpha. The feature works but is still evolving.
Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts.
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use defineSkill() to define skills with built-in validation:
src/skills/example-skill.ts
Key points:
  • name is a unique identifier string for the skill (kebab-case recommended).
  • label is the human-readable display name shown in the UI.
  • content contains the skill instructions — this is the text the AI agent uses.
  • icon (optional) sets the icon displayed in the UI.
  • description (optional) provides additional context about the skill’s purpose.
Agents are AI assistants that live inside your workspace. Use defineAgent() to create agents with a custom system prompt:
src/agents/example-agent.ts
Key points:
  • name is the unique identifier string for the agent (kebab-case recommended).
  • label is the display name shown in the UI.
  • prompt is the system prompt that defines the agent’s behavior.
  • description (optional) provides context about what the agent does.
  • icon (optional) sets the icon displayed in the UI.
  • modelId (optional) overrides the default AI model used by the agent.
  • responseFormat (optional) controls the shape of the agent’s output. Defaults to { type: 'text' } for free-form text. Use { type: 'json', schema } to force structured JSON output.
By default an agent returns free-form text. To get structured output, set responseFormat to { type: 'json' } and provide a schema:
src/agents/structured-agent.ts
Schema notes:
  • The schema is a flat object: each property’s type must be a primitive (string, number, or boolean). Nested objects and arrays are not supported.
  • description (optional) on each property guides the model on what to put there.
  • required (optional) lists the properties the model must always return.
  • additionalProperties: false (optional) forbids any property not declared in properties.
runAgent() lets a logic function run one of your app’s agents (with its skills and tools). Identify the agent by the universalIdentifier you passed to defineAgent(). Pass either a prompt string or a messages conversation history — not both:
src/logic-functions/run-enricher.ts
For multi-turn bots (Slack, Discord, Teams, …), pass thread history as messages instead of a single prompt:
src/logic-functions/reply-in-thread.ts
Key points:
  • Provide exactly one of prompt (string) or messages (1 to 100 entries of { role: 'user' | 'assistant', content: string }).
  • The agent runs synchronously and can read/update records itself via its own tools — runAgent() resolves once the run completes.
  • An app can only run its own agents.
  • The app’s default role must grant the AI permission flag — add SystemPermissionFlag.AI to its permissionFlagUniversalIdentifiers (or set canAccessAllTools: true). Without it, runAgent() fails with a permission error.
  • Set a generous timeoutSeconds on the logic function — agent runs can take several seconds.
  • success is true and result is non-null when the run completes; on failure success is false, result is null, and error holds the reason (for example, when the workspace ran out of AI credits mid-run).
src/roles/default-role.ts
Avoid loops: if you call runAgent() from a *.updated database-event trigger and the agent updates the same record, scope the trigger with updatedFields to a field the agent never writes (e.g. the source URL), or guard on whether any target field is still empty before calling runAgent().

Running on behalf of a workspace member

Pass runAsWorkspaceMemberId when the run is triggered by a person — a chat bot answering a message, for instance — so the agent acts as that member instead of as the app:
src/logic-functions/answer-question.ts
The run then acts with the member’s own role: it can do whatever that member can do and nothing more, records it creates are attributed to them, and their row-level permissions apply. The agent’s role does not participate — it is the app’s default for runs with nobody behind them. Omit the field for autonomous runs (scheduled jobs, database-event triggers): those keep the agent’s own role. The acting application stays attached to the run’s context for provenance, without narrowing the member’s permissions.Your app is responsible for mapping the person who triggered the run to a workspace member. Naming one requires an app access token, and what a token may name depends on whether it carries a user:
  • A token with no user attached may name any member. A logic function runs with one, and so do tokens minted through client_credentials or from an API key.
  • A token issued on behalf of a user, as a front component receives, may only name that user’s own member.
Anything else — a plain user session, an API key without an app token — may not name a member at all.
runAgent() throws when the workspace member cannot be resolved — an unknown or removed member, or one with no role. It never falls back to the agent’s own role, since that would grant more access than the caller asked for.