# Struere Documentation — SDK > Filtered section from the Struere docs. Full docs: https://docs.struere.dev/llms.txt --- ## SDK Overview > TypeScript SDK for defining agents, data, roles, and automations Source: https://docs.struere.dev/sdk/overview.md The Struere SDK provides 6 TypeScript definition functions — defineAgent, defineData, defineRole, defineTrigger, defineRouter, and defineTools — for configuring an entire AI agent system as code. Resources are upserted by slug on sync, with pre-sync validation of agent references, entity type references, and integration requirements. ## Installation ```bash bun add struere ``` Initialize a new project: ```bash bunx struere init ``` This scaffolds the following project structure: ``` my-org/ ├── struere.json ├── agents/ │ └── scheduler.ts ├── entity-types/ │ └── teacher.ts ├── roles/ │ └── admin.ts ├── triggers/ │ └── notify-on-session.ts └── tools/ └── index.ts ``` The `struere.json` file stores organization metadata: ```json { "version": "2.0", "organization": { "id": "org_abc123", "slug": "acme-corp", "name": "Acme Corp" } } ``` ## SDK Exports The SDK exports five definition functions, each responsible for a specific resource type: ```typescript import { defineAgent, defineTools, defineData, defineRole, defineTrigger, defineRouter } from 'struere' ``` | Function | Purpose | File Location | |----------|---------|---------------| | `defineAgent` | Create and configure AI agent definitions | `agents/*.ts` | | `defineData` | Define data type schemas | `entity-types/*.ts` | | `defineRole` | Create roles with policies, scope rules, and field masks | `roles/*.ts` | | `defineTrigger` | Define automation rules | `triggers/*.ts` | | `defineRouter` | Define conversation routing rules | `routers/*.ts` | | `defineTools` | Create custom tool handlers | `tools/index.ts` | Each definition file exports a default using its corresponding function: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Scheduler", slug: "scheduler", version: "0.1.0", systemPrompt: "You are a scheduling assistant.", tools: ["entity.create", "entity.query", "event.emit"], }) ``` ## Type Exports The SDK also exports all TypeScript types for use in your project: ```typescript import type { AgentConfig, ModelConfig, DataTypeConfig, JSONSchema, JSONSchemaProperty, RoleConfig, PolicyConfig, ScopeRuleConfig, FieldMaskConfig, TriggerConfig, TriggerAction, ToolReference, ToolParameters, ParameterDefinition, ToolHandler, ToolContext, StruereSDK, StruereProject, SyncPayload, SyncState, } from 'struere' ``` ## Organization-Centric Architecture Struere uses a single-project approach where one repository defines the entire organization's AI infrastructure: - **Agents** share data types, roles, and tools across the organization - **Data types** define the domain schema once and are available to all agents - **Roles** enforce access control consistently across all agents and API access - **Automations** automate workflows that fire from any mutation source (dashboard, agents, or API) - **Tools** are org-level resources in the `customTools` table, referenced by any agent by name The `struere dev` command watches all directories and syncs changes to the Convex backend in real time. The `struere deploy` command pushes all agents to production. ## Sync Workflow During development, all resources are synced as a single payload: ```typescript { agents: [...], entityTypes: [...], roles: [...], triggers: [...], tools: [...] } ``` Resources are upserted by their `slug` (agents, data types, triggers) or `name` (roles), so renaming a slug creates a new resource rather than updating the existing one. --- ## defineData > Define data type schemas for your domain Source: https://docs.struere.dev/sdk/define-data.md The `defineData` function creates and validates data type schema definitions. Each data type is defined in its own file under the `entity-types/` directory. ```typescript import { defineData } from 'struere' export default defineData({ name: "Teacher", slug: "teacher", schema: { type: "object", properties: { name: { type: "string", description: "Full name" }, email: { type: "string", format: "email", description: "Email address" }, subjects: { type: "array", items: { type: "string" }, description: "Subjects they can teach", }, hourlyRate: { type: "number", description: "Rate per hour in cents" }, }, required: ["name", "email"], }, searchFields: ["name", "email"], displayConfig: { titleField: "name", subtitleField: "email", }, }) ``` ## EntityTypeConfig | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Display name for the data type | | `slug` | `string` | Yes | URL-safe identifier, used in API queries and tool calls | | `schema` | `JSONSchema` | Yes | JSON Schema definition for the data | | `searchFields` | `string[]` | No | Fields indexed for text search (defaults to `[]`) | | `displayConfig` | `object` | No | Controls how records are displayed in the dashboard | | `boundToRole` | `string` | No | Binds this data type to a role name for automatic user linking | | `userIdField` | `string` | No | Field that stores the Clerk user ID (defaults to `"userId"` when `boundToRole` is set) | ### Validation `defineData` throws errors if: - `name`, `slug`, or `schema` is missing - `schema.type` is not `"object"` - Any nested object property is missing `properties` - `boundToRole` is an empty string - `userIdField` is set without `boundToRole` ## JSON Schema Data type schemas use the JSON Schema format with the following type system: ```typescript interface JSONSchema { type: 'object' properties: Record required?: string[] } interface JSONSchemaProperty { type: 'string' | 'number' | 'boolean' | 'array' | 'object' description?: string format?: string enum?: string[] items?: JSONSchemaProperty properties?: Record required?: string[] references?: string } ``` The root schema must always be `type: "object"`. Nested objects must declare their `properties`. ### Supported Property Types **String fields:** ```typescript { name: { type: "string", description: "Full name" }, email: { type: "string", format: "email" }, status: { type: "string", enum: ["active", "inactive", "suspended"], }, } ``` **Number fields:** ```typescript { hourlyRate: { type: "number", description: "Rate in cents" }, age: { type: "number" }, } ``` **Boolean fields:** ```typescript { isActive: { type: "boolean", description: "Whether the record is active" }, } ``` **Array fields:** ```typescript { subjects: { type: "array", items: { type: "string" }, description: "List of subjects", }, tags: { type: "array", items: { type: "string", enum: ["math", "science", "english"] }, }, } ``` **Nested object fields:** ```typescript { address: { type: "object", properties: { street: { type: "string" }, city: { type: "string" }, postalCode: { type: "string" }, }, required: ["street", "city"], }, } ``` ## References Fields with `references` enforce foreign key constraints. When an entity is created or updated, any field with `references` is validated to ensure the referenced entity exists. ```typescript import { defineData } from 'struere' export default defineData({ name: "Session", slug: "session", schema: { type: "object", properties: { studentId: { type: "string", references: "student" }, teacherId: { type: "string", references: "teacher" }, startTime: { type: "number" }, duration: { type: "number" }, subject: { type: "string" }, }, required: ["studentId", "teacherId", "startTime", "duration"], }, }) ``` When the agent calls `entity.create` or `entity.update` with a `studentId` or `teacherId` value, the platform validates that: - The referenced entity exists - The referenced entity is not deleted - The referenced entity belongs to the same organization and environment - The referenced entity is of the correct entity type (e.g., `studentId` must reference a `student` entity) If any validation fails, the operation throws an error identifying the invalid reference field. ## Display Configuration The `displayConfig` field controls how records appear in the dashboard: ```typescript displayConfig: { titleField: "name", subtitleField: "email", descriptionField: "notes", } ``` | Field | Description | |-------|-------------| | `titleField` | Primary display field (shown as heading) | | `subtitleField` | Secondary display field (shown below title) | | `descriptionField` | Extended description field | ## Search Fields The `searchFields` array specifies which fields are indexed for text search via `entity.query`: ```typescript searchFields: ["name", "email", "phone"] ``` When an agent uses `entity.query` with a search term, only these fields are matched. ## Role Binding The `boundToRole` and `userIdField` fields create an automatic link between a data type and a user role. When a user with the bound role logs in, they are associated with the matching record: ```typescript export default defineData({ name: "Teacher", slug: "teacher", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string", format: "email" }, userId: { type: "string", description: "Clerk user ID" }, }, required: ["name", "email"], }, boundToRole: "teacher", userIdField: "userId", }) ``` When `boundToRole` is set and `userIdField` is omitted, it defaults to `"userId"`. ## Filtering entities Entity queries (via the Data API, `entity.query` tool, or the JS client) accept a `filters` object. Two rules govern which fields are filterable: - **Top-level fields are restricted to indexed columns.** The Data API only accepts top-level filter fields that are indexed on the entity table (e.g., `matchId` when present, plus a small set of system fields). Filtering on a non-indexed top-level field returns `400 Bad Request` with the list of fields that ARE queryable. - **Use `data.` for everything in the JSON payload.** Domain fields declared in your `schema.properties` live inside the entity's `data` blob. Reference them as `data.status`, `data.teamId`, `data.guardianId`, etc. These filters apply in-memory after the indexed scan. ```typescript await struere.data.query('session', { filters: { 'data.status': 'scheduled', 'data.teacherId': 'usr_123', }, }) ``` ### Foot-gun: top-level `status` is the entity lifecycle column The top-level `status` field on an entity is the platform-managed lifecycle column (`active`, `archived`, `deleted`). If your data type also defines a domain `status` field (e.g., `pending`, `confirmed`, `paid`), they are not the same column. A top-level `status` filter is **rejected** with `400 Bad Request`. The error tells you to use `data.status` instead: ```typescript await struere.data.query('session', { filters: { 'data.status': 'scheduled' }, }) ``` If you really do want to filter the lifecycle column, pass `status` as a top-level option (alongside `filters`, `limit`, `cursor`) — not as a key inside `filters`. ### Foot-gun: filtering on a non-indexed top-level field If you write `filters: { teacherId: 'usr_123' }` and `teacherId` is a domain field stored in the JSON payload, the API returns `400 Bad Request` listing the indexed fields it accepts. The fix is the same: prefix with `data.`: ```typescript await struere.data.query('session', { filters: { 'data.teacherId': 'usr_123' }, }) ``` ## Unsupported JSON Schema features `defineData` accepts a deliberate subset of JSON Schema. The fields shown in the type definition above (`type`, `description`, `format`, `enum`, `items`, `properties`, `required`, `references`) are the only ones recognised — anything else will be rejected by the SDK type or silently dropped. - `additionalProperties` — schemas are closed. To persist a `Record` shape, reshape it into an array of records with an `id` field. - `oneOf` / `anyOf` / `allOf` — model variants as a discriminated union via an `enum` field, then branch on that field in your code. - `if` / `then` / `else` — conditional shapes are not supported. Express conditional logic in the application layer or split into separate entity types. - `$ref` / `definitions` — schemas are inlined. Copy shared shapes into each entity type that needs them. - `pattern`, `minimum`, `maximum`, `minLength`, `maxLength`, `multipleOf`, etc. — only `enum` and `format` constrain values at the schema level. Enforce additional validation in your code or via tools. ## Full Examples ### Student Data Type ```typescript import { defineData } from 'struere' export default defineData({ name: "Student", slug: "student", schema: { type: "object", properties: { name: { type: "string" }, grade: { type: "string" }, subjects: { type: "array", items: { type: "string" }, }, notes: { type: "string" }, guardianId: { type: "string" }, preferredTeacherId: { type: "string" }, }, required: ["name"], }, searchFields: ["name"], displayConfig: { titleField: "name", subtitleField: "grade", }, }) ``` ### Session Data Type ```typescript import { defineData } from 'struere' export default defineData({ name: "Session", slug: "session", schema: { type: "object", properties: { teacherId: { type: "string" }, studentId: { type: "string" }, guardianId: { type: "string" }, startTime: { type: "number", description: "Unix timestamp" }, duration: { type: "number", description: "Duration in minutes" }, subject: { type: "string" }, status: { type: "string", enum: [ "pending_payment", "scheduled", "in_progress", "completed", "cancelled", "no_show", ], }, notes: { type: "string" }, teacherReport: { type: "string" }, }, required: ["teacherId", "studentId", "guardianId", "startTime", "duration"], }, searchFields: ["subject"], displayConfig: { titleField: "subject", subtitleField: "status", }, }) ``` ### Entitlement Data Type (Credits System) ```typescript import { defineData } from 'struere' export default defineData({ name: "Entitlement", slug: "entitlement", schema: { type: "object", properties: { guardianId: { type: "string" }, studentId: { type: "string" }, totalCredits: { type: "number" }, remainingCredits: { type: "number" }, expiresAt: { type: "number", description: "Unix timestamp" }, }, required: ["guardianId", "studentId", "totalCredits", "remainingCredits"], }, displayConfig: { titleField: "remainingCredits", subtitleField: "totalCredits", }, }) ``` --- ## defineRole > Create roles with policies, scope rules, and field masks Source: https://docs.struere.dev/sdk/define-role.md The `defineRole` function creates roles with access control policies, row-level scope rules, and column-level field masks. Each role is defined in its own file under the `roles/` directory. ```typescript import { defineRole } from 'struere' export default defineRole({ name: "teacher", description: "Tutors who conduct sessions", agentAccess: ["scheduling-agent", "student-portal"], policies: [ { resource: "session", actions: ["list", "read", "update"], effect: "allow" }, { resource: "student", actions: ["list", "read"], effect: "allow" }, { resource: "payment", actions: ["*"], effect: "deny" }, ], scopeRules: [ { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" }, ], fieldMasks: [ { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" }, { entityType: "student", fieldPath: "data.guardianId", maskType: "hide" }, ], }) ``` ## RoleConfig | Field | Type | Required | Description | |-------|------|----------|-------------| | `slug` | `string` | No | Unique role identifier (lowercase, alphanumeric + hyphens). Derived from `name` if omitted. Will be required in a future major version — declare it explicitly on new roles. | | `name` | `string` | Yes | Display label for the role | | `description` | `string` | No | Human-readable description | | `agentAccess` | `string[]` | No | Agent slugs this role can access conversations for (defaults to `[]`) | | `policies` | `PolicyConfig[]` | Yes | Access control rules (at least one required) | | `scopeRules` | `ScopeRuleConfig[]` | No | Row-level security filters (defaults to `[]`) | | `fieldMasks` | `FieldMaskConfig[]` | No | Column-level security masks (defaults to `[]`) | ### Slug vs name `slug` is the role's identity. It is what agents reference in `defineAgent({ roles: ['coach'] })` and what scope rules and dashboard ACLs match against. `name` is the human-readable label shown in the dashboard. ```typescript import { defineRole } from 'struere' export default defineRole({ slug: 'coach', name: 'Coach', policies: [ { resource: 'player', actions: ['read', 'update'], effect: 'allow' }, ], }) ``` When `slug` is omitted, it is derived from `name` (lowercased, non-alphanumeric replaced with hyphens). Existing roles without a `slug` continue to work — they get one auto-derived on next sync. New roles should declare `slug` explicitly; it will be required in a future major version. See [`defineAgent`](./define-agent#roles-and-permissions) for how agents reference roles by slug. ### Validation `defineRole` throws errors if: - `name` is missing - `policies` is empty or missing - Any policy is missing `resource`, `actions`, or `effect` - `agentAccess` contains empty strings ## PolicyConfig Policies define what actions a role can perform on which resources. ```typescript interface PolicyConfig { resource: string actions: string[] effect: 'allow' | 'deny' } ``` | Field | Type | Description | |-------|------|-------------| | `resource` | `string` | Data type slug or built-in resource (`users`) the policy applies to | | `actions` | `string[]` | Allowed values: `"create"`, `"read"`, `"update"`, `"delete"`, `"list"`, or `"*"` for all | | `effect` | `'allow' \| 'deny'` | Whether to allow or deny the specified actions | ### Policy Evaluation The permission engine evaluates policies with a **deny-overrides-allow** model: 1. All policies matching the resource and action are collected 2. If any matching policy has `effect: "deny"`, access is denied 3. If at least one policy has `effect: "allow"` and none deny, access is allowed 4. If no policies match, access is denied ```typescript policies: [ { resource: "session", actions: ["list", "read", "update"], effect: "allow" }, { resource: "session", actions: ["delete"], effect: "deny" }, ] ``` In this example, the role can list, read, and update sessions but cannot delete them. Even if another role grants delete access, this deny policy overrides it. ### Wildcard Actions Use `"*"` to match all actions on a resource: ```typescript { resource: "session", actions: ["*"], effect: "allow" } ``` This grants create, read, update, delete, and list access. ### Deny-All Pattern Block all access to a resource: ```typescript { resource: "payment", actions: ["*"], effect: "deny" } ``` ## ScopeRuleConfig Scope rules implement row-level security by filtering which entities a role can see. ```typescript interface ScopeRuleConfig { entityType: string field: string operator: 'eq' | 'neq' | 'in' | 'contains' value: string } ``` | Field | Type | Description | |-------|------|-------------| | `entityType` | `string` | Data type slug to filter | | `field` | `string` | Dot-notation path to the entity field (e.g., `"data.teacherId"`) | | `operator` | `string` | Comparison operator | | `value` | `string` | Value to compare against; supports `"actor.userId"` for dynamic resolution | ### Dynamic Value Resolution The `value` field supports the special prefix `actor.` to reference the current actor's properties at runtime: - `"actor.userId"` resolves to the authenticated user's ID - This enables scope rules like "a teacher can only see their own sessions" ### Scope Rule Examples Teacher sees only sessions where they are the assigned teacher: ```typescript scopeRules: [ { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId", }, ] ``` Guardian sees only their own children: ```typescript scopeRules: [ { entityType: "student", field: "data.guardianId", operator: "eq", value: "actor.userId", }, { entityType: "session", field: "data.guardianId", operator: "eq", value: "actor.userId", }, ] ``` ### Operators | Operator | Description | |----------|-------------| | `eq` | Field equals value | | `neq` | Field does not equal value | | `in` | Field is contained in value (array) | | `contains` | Field contains value (substring or array member) | > **Gotcha:** Use `neq`, not `ne`. Other operators (`ne`, `nin`, `gt`, etc.) silently fail to match. See [Platform Gotchas](/platform/gotchas) for details. ## FieldMaskConfig Field masks implement column-level security by hiding or redacting specific fields. ```typescript interface FieldMaskConfig { entityType: string fieldPath: string maskType: 'hide' | 'redact' maskConfig?: Record } ``` | Field | Type | Description | |-------|------|-------------| | `entityType` | `string` | Data type slug to mask | | `fieldPath` | `string` | Dot-notation path to the field (e.g., `"data.paymentId"`) | | `maskType` | `'hide' \| 'redact'` | `hide` removes the field entirely; `redact` replaces the value | | `maskConfig` | `object` | Additional configuration for redaction behavior | ### Mask Types **Hide** removes the field from the response entirely: ```typescript { entityType: "student", fieldPath: "data.guardianId", maskType: "hide" } ``` **Redact** replaces the field value while keeping the key present: ```typescript { entityType: "payment", fieldPath: "data.amount", maskType: "redact", maskConfig: { replacement: "***" } } ``` ### Allowlist Strategy Field masks use an **allowlist strategy**, which means new fields added to a data type are hidden by default until explicitly allowed. This is a fail-safe approach that prevents accidental data exposure. > **Gotcha:** Adding a new field to a data type means it's hidden by default for any role with a field mask on that type — review masks when you change schemas. See [Platform Gotchas](/platform/gotchas) for details. ## Agent Access The `agentAccess` field controls which agents' conversations a role can see in the dashboard. Members with a role can only view and reply to threads belonging to the listed agents. Admins bypass this restriction and see all conversations. ```typescript agentAccess: ["sales-agent", "support-agent"] ``` ### `agentAccess` vs being assigned to an agent's `roles` A role document defines two independent relationships with agents. They are not the same thing. | Relationship | Field | What it does | |--------------|-------|--------------| | Dashboard ACL | `agentAccess` on the role | Lets human users with this role view and reply to threads belonging to the listed agents in the dashboard. **Does not grant the agent any permissions.** | | Permission inheritance | `roles` on the agent (see [`defineAgent`](./define-agent#roles-and-permissions)) | When the agent executes, it inherits this role's `policies`, `scopeRules`, and `fieldMasks`. The agent acts under those permissions. | Same role doc, two different relationships. Pick the one(s) you need. The example below uses both: humans assigned the `coach` role can chat with `coach-stats` in the dashboard (`agentAccess`), and the `coach-stats` agent itself inherits the role's policies and scope rules at runtime (when it lists `roles: ["coach"]` in `defineAgent`). ```typescript import { defineRole } from 'struere' export default defineRole({ name: "coach", policies: [ { resource: "player", actions: ["read", "update"], effect: "allow" }, ], scopeRules: [ { entityType: "player", field: "data.teamId", operator: "eq", value: "team-A" }, ], agentAccess: ["coach-stats"], }) ``` For the full picture of how roles are granted to humans and agents and evaluated at runtime, see [Agent Permissions](../platform/agent-permissions). | Behavior | Description | |----------|-------------| | Not set or empty | Role has no conversation access | | List of slugs | Role can view threads for those agents | | Multiple roles | Access is the union of all assigned roles' `agentAccess` slugs | | Admin users | Bypass `agentAccess` entirely — see all conversations | Agent slugs are resolved at query time. If a slug doesn't match an existing agent, it is silently skipped. When the agent is created later, access is automatically granted. Members cannot start new conversations — they can only view and reply to existing threads for their allowed agents. ## Full Examples ### Admin Role ```typescript import { defineRole } from 'struere' export default defineRole({ name: "admin", description: "Full access to all resources", policies: [ { resource: "teacher", actions: ["*"], effect: "allow" }, { resource: "student", actions: ["*"], effect: "allow" }, { resource: "guardian", actions: ["*"], effect: "allow" }, { resource: "session", actions: ["*"], effect: "allow" }, { resource: "payment", actions: ["*"], effect: "allow" }, { resource: "entitlement", actions: ["*"], effect: "allow" }, ], }) ``` ### Teacher Role ```typescript import { defineRole } from 'struere' export default defineRole({ name: "teacher", description: "Tutors who conduct sessions", agentAccess: ["scheduling-agent", "student-portal"], policies: [ { resource: "session", actions: ["list", "read", "update"], effect: "allow" }, { resource: "student", actions: ["list", "read"], effect: "allow" }, { resource: "teacher", actions: ["read", "update"], effect: "allow" }, { resource: "payment", actions: ["*"], effect: "deny" }, { resource: "entitlement", actions: ["*"], effect: "deny" }, ], scopeRules: [ { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" }, { entityType: "teacher", field: "data.userId", operator: "eq", value: "actor.userId" }, ], fieldMasks: [ { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" }, { entityType: "student", fieldPath: "data.guardianId", maskType: "hide" }, ], }) ``` ### Guardian Role ```typescript import { defineRole } from 'struere' export default defineRole({ name: "guardian", description: "Parents or guardians of students", agentAccess: ["parent-portal"], policies: [ { resource: "student", actions: ["list", "read", "update"], effect: "allow" }, { resource: "session", actions: ["list", "read"], effect: "allow" }, { resource: "payment", actions: ["list", "read"], effect: "allow" }, { resource: "entitlement", actions: ["list", "read"], effect: "allow" }, { resource: "teacher", actions: ["*"], effect: "deny" }, ], scopeRules: [ { entityType: "student", field: "data.guardianId", operator: "eq", value: "actor.userId" }, { entityType: "session", field: "data.guardianId", operator: "eq", value: "actor.userId" }, { entityType: "payment", field: "data.guardianId", operator: "eq", value: "actor.userId" }, { entityType: "entitlement", field: "data.guardianId", operator: "eq", value: "actor.userId" }, ], fieldMasks: [ { entityType: "session", fieldPath: "data.teacherReport", maskType: "hide" }, ], }) ``` ### Team Lead Role ```typescript import { defineRole } from 'struere' export default defineRole({ name: "team-lead", description: "Team lead with member management access", agentAccess: ["support-agent", "sales-agent"], policies: [ { resource: "users", actions: ["create", "update", "delete"], effect: "allow" }, { resource: "session", actions: ["list", "read", "update"], effect: "allow" }, { resource: "customer", actions: ["list", "read"], effect: "allow" }, ], scopeRules: [ { entityType: "session", field: "data.teamLeadId", operator: "eq", value: "actor.userId" }, ], }) ``` The `resource: "users"` policies grant this role permission to invite new members (`create`), assign internal roles to team members (`update`), and remove non-admin members (`delete`) from the Team page in the dashboard. Non-admins can only invite as `org:member`. Team leads cannot promote users to admin or modify admin users. --- ## defineAgent > Create and configure AI agent definitions Source: https://docs.struere.dev/sdk/define-agent.md The `defineAgent` function creates and validates an AI agent configuration. Each agent is defined in its own file under the `agents/` directory. ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Scheduler", slug: "scheduler", version: "0.1.0", systemPrompt: "You are a scheduling assistant for {{organizationName}}.", model: { model: "openai/gpt-5-mini", }, tools: [ "entity.create", "entity.query", "entity.update", "agent.chat", ], }) ``` ## AgentConfig | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Display name for the agent | | `slug` | `string` | Yes | URL-safe identifier, used for API routing (`/v1/agents/:slug/chat`) | | `version` | `string` | Yes | Semantic version string (e.g., `"0.1.0"`) | | `systemPrompt` | `string \| (() => string \| Promise)` | Yes | System prompt with template variable support | | `description` | `string` | No | Human-readable description | | `model` | `ModelConfig` | No | LLM model and inference settings | | `tools` | `string[]` | No | Array of tool names (built-in and custom) | | `roles` | `string[]` | No | Role names whose policies, scope rules, and field masks the agent inherits at runtime | | `firstMessageSuggestions` | `string[]` | No | Clickable suggestion chips shown in the chat empty state | | `threadContextParams` | `ThreadContextParam[]` | No | Schema for expected thread context parameters (see below) | ### Validation `defineAgent` throws an error if `name`, `version`, or `systemPrompt` is missing. ## Model Configuration The `model` field configures which LLM provider and model the agent uses. If omitted, defaults to `openai/gpt-5-mini` with temperature `0.7` and maxTokens `4096`. ```typescript interface ModelConfig { model: string temperature?: number maxTokens?: number reasoning?: { enabled?: boolean effort?: 'minimal' | 'low' | 'medium' | 'high' budgetTokens?: number hideFromResponse?: boolean } } ``` Model IDs use `"provider/model-name"` format (e.g., `"openai/gpt-5-mini"`, `"anthropic/claude-sonnet-4-6"`). > **Gotcha:** Bare model names like `"gpt-5-mini"` (without the `openai/` prefix) are rejected at runtime — always include the provider. See [Platform Gotchas](/platform/gotchas) for details. ```typescript export default defineAgent({ name: "Analyst", slug: "analyst", version: "1.0.0", systemPrompt: "You are a precise data analyst.", model: { model: "openai/gpt-5-mini", temperature: 0.3, maxTokens: 8192, }, tools: ["entity.query"], }) ``` To enable chain-of-thought reasoning: ```typescript export default defineAgent({ name: "Research Assistant", slug: "research-assistant", version: "1.0.0", systemPrompt: "You are a research assistant.", model: { model: "openai/gpt-5-mini", reasoning: { effort: "high", }, }, tools: [], }) ``` For the full list of providers, models, pricing, and reasoning options, see [Model Configuration](../reference/model-configuration). ## Tools The `tools` field is an array of tool name strings referencing both [built-in tools](../tools/built-in-tools) and [custom tools](../tools/custom-tools) defined via `defineTools`. Custom tools are org-level resources — agents reference them by name rather than embedding full tool definitions. Tools marked as `templateOnly: true` are available to all agents' system prompts automatically and do not need to be listed here. ```typescript tools: [ "entity.create", "entity.query", "send_email", ] ``` ## Roles and Permissions The `roles` field declares which roles' policies, scope rules, and field masks the agent inherits when it executes. Roles are referenced by name and must exist in `roles/`. ```typescript export default defineAgent({ name: "Coach Stats", slug: "coach-stats", version: "0.1.0", systemPrompt: "You report stats for Team A players.", model: { model: "openai/gpt-5-mini" }, tools: ["entity.query"], roles: ["team-a-coach"], }) ``` When this agent runs, every `entity.query`, `entity.update`, and other permission-checked tool call is evaluated against the union of the listed roles' policies, scope rules, and field masks. The agent sees only the rows and fields its roles allow. If `roles` is empty or missing, the agent falls back to the singleton `agent` role — existing zero-config agents keep working unchanged. This is distinct from [`agentAccess`](../sdk/define-role#agent-access) on a role, which is a dashboard ACL (which users can chat with which agents) and grants the agent no permissions. See [Agent Permissions](../platform/agent-permissions) for the end-to-end model. ## First Message Suggestions The `firstMessageSuggestions` field provides an array of strings displayed as clickable chips in the chat empty state. When a user clicks a suggestion, it sends that text as their first message. ```typescript export default defineAgent({ name: "Support", slug: "support", version: "0.1.0", systemPrompt: "You are a support agent for {{organizationName}}.", model: { model: "openai/gpt-5-mini" }, tools: ["entity.query"], firstMessageSuggestions: [ "What can you help me with?", "Show me recent activity", "Create a new record", ], }) ``` Suggestions appear in all chat surfaces: the dashboard dev chat, public chat, embedded widget, and the chat sidebar on the agent detail page. Agents without suggestions show the default "Start a conversation" empty state. ## Thread Context Parameters The `threadContextParams` field declares what context parameters your agent expects from callers. When defined, the backend validates incoming params — checking required fields, enforcing types, and dropping unknown params. ```typescript interface ThreadContextParam { name: string type: 'string' | 'number' | 'boolean' required?: boolean description?: string } ``` ```typescript export default defineAgent({ name: "Support", slug: "support", version: "0.1.0", systemPrompt: `You are a support agent for {{organizationName}}. Customer: {{threadContext.params.email}} Plan: {{threadContext.params.plan}}`, model: { model: "openai/gpt-5-mini" }, tools: ["entity.query"], threadContextParams: [ { name: "email", type: "string", required: true, description: "Customer email" }, { name: "plan", type: "string", description: "Subscription plan" }, ], }) ``` These parameters are passed differently depending on the channel: | Channel | How params are passed | |---------|----------------------| | **Widget** | URL parameters on the `