# Struere Documentation (Full) > This file contains the complete Struere documentation. --- # Getting Started --- ## Introduction > What is Struere and why use it Struere is a **permission-aware AI agent platform** that lets you build, deploy, and manage intelligent agents with fine-grained access control. Every operation your agents perform is governed by role-based access control (RBAC) with row-level security (scope rules) and column-level security (field masks), ensuring that agents and users only access the data they are authorized to see. ## Platform Architecture Struere is organized as a monorepo with three main layers: ``` apps/ packages/ platform/ ├── dashboard (Next.js) └── struere (SDK + CLI) ├── convex (Backend) └── web (Marketing) └── tool-executor (Sandboxed) ``` - **Dashboard** — A Next.js 14 application providing a real-time admin interface for managing agents, entities, roles, permissions, and integrations. - **SDK + CLI** — The `struere` package gives you `defineAgent`, `defineEntityType`, `defineRole`, `defineTrigger`, and other helpers to define your platform configuration as code. The CLI syncs these definitions to your backend. - **Convex Backend** — The core backend powering real-time data, permission evaluation, agent execution, and tool orchestration. - **Tool Executor** — A sandboxed server that runs custom tool handlers with a restricted fetch allowlist. ## Key Capabilities ### Role-Based Access Control (RBAC) Define roles with granular policies that control which resources each role can access and what actions they can perform. Struere supports five action types: `create`, `read`, `update`, `delete`, and `list`. ```typescript import { defineRole } from 'struere' export default defineRole({ name: "teacher", description: "Tutors who conduct sessions", policies: [ { resource: "session", actions: ["list", "read", "update"], effect: "allow" }, { resource: "payment", actions: ["*"], effect: "deny" }, ], }) ``` ### Row-Level Security (Scope Rules) Scope rules filter data at the row level so users only see records they own or are assigned to. ```typescript scopeRules: [ { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" }, ] ``` ### Column-Level Security (Field Masks) Field masks use an allowlist strategy to control which fields are visible to each role. New fields are hidden by default, making the system fail-safe. ```typescript fieldMasks: [ { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" }, ] ``` ### Entity Management Define structured entity types with JSON schemas, then create, query, update, and relate entities through both the dashboard and agent tool calls. ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ name: "Teacher", slug: "teacher", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string", format: "email" }, hourlyRate: { type: "number" }, }, required: ["name", "email"], }, searchFields: ["name", "email"], }) ``` ### Multi-Agent Communication Agents can delegate tasks to other agents using the `agent.chat` built-in tool. The platform enforces a depth limit of 3 and detects cycles to prevent infinite loops. ```typescript export default defineAgent({ name: "Coordinator", slug: "coordinator", tools: ["entity.query", "agent.chat"], systemPrompt: "You coordinate between specialized agents...", }) ``` ### Environment Isolation All data, roles, configurations, and permissions are fully isolated between `development` and `production` environments. Development API keys cannot access production data. The CLI's `dev` command syncs to the development environment, while `deploy` pushes to production. ### Triggers and Automations Define event-driven automations that fire when entities are created, updated, or deleted. Triggers support scheduling, retries, and template variable resolution. ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Notify on New Session", slug: "notify-on-session", on: { entityType: "session", action: "created", condition: { "data.status": "scheduled" }, }, actions: [ { tool: "event.emit", args: { eventType: "session.notification", entityId: "{{trigger.entityId}}", }, }, ], }) ``` ## Tech Stack | Technology | Role | |------------|------| | **Next.js 14** | Dashboard and web applications | | **Convex** | Real-time backend with native subscriptions | | **Tool Executor** | Sandboxed custom tool execution | | **Clerk** | Authentication and organization management | | **TypeScript** | End-to-end type safety | | **Bun** | Package management and runtime | | **Anthropic Claude** | Default LLM provider for agents | ## How It Works ``` Define agents, roles, entity types as code │ ▼ CLI syncs definitions to Convex backend │ ▼ Agents receive messages via API or dashboard │ ▼ Permission engine evaluates access on every operation │ ▼ Agents use built-in and custom tools (permission-checked) │ ▼ Results are persisted, events are logged, triggers fire ``` Every request flows through the permission engine: an `ActorContext` is built with the caller's organization, roles, and environment. Policies are evaluated with a deny-overrides-allow model. Scope rules filter query results. Field masks strip unauthorized fields from responses. ## Next Steps - [Getting Started](./getting-started) — Install Struere and create your first agent - [CLI Overview](./cli/overview) — Learn the command-line interface - [Agent Configuration](./sdk/define-agent) — Deep dive into agent configuration - [Permissions](./platform/permissions) — Understand the permission engine --- ## Getting Started > Install Struere and create your first agent ## Prerequisites Before you begin, make sure you have: - **Node.js 18+** installed - **Bun** installed (`curl -fsSL https://bun.sh/install | bash`) — used as the package manager - A **Struere account** — sign up at [app.struere.dev](https://app.struere.dev) (uses Clerk for authentication) - An **Anthropic API key** — set on your Convex deployment as the `ANTHROPIC_API_KEY` environment variable Struere is a hosted platform backed by Convex. When you create an account and organization, your Convex deployment is provisioned automatically. ## Installation Install the Struere package as a project dependency: ```bash npm install struere ``` ## Initialize a Project Run the init command to scaffold an organization-centric project: ```bash npx struere init ``` This command will: 1. Open a browser for authentication (sign in with your Struere account) 2. Prompt you to select an organization 3. Create the project directory structure 4. Write a `struere.json` configuration file with your organization details 5. Run `bun install` to install dependencies ### Project Structure After initialization, your project will have this structure: ``` my-org/ ├── struere.json ├── agents/ │ └── (your agent definitions) ├── entity-types/ │ └── (your entity type schemas) ├── roles/ │ └── (your role definitions with policies) ├── triggers/ │ └── (your trigger automations) └── tools/ └── index.ts ``` ### struere.json The configuration file identifies your organization: ```json { "version": "2.0", "organization": { "id": "org_abc123", "slug": "acme-corp", "name": "Acme Corp" } } ``` ## Create Your First Agent Create a file at `agents/my-agent.ts`: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "My First Agent", slug: "my-first-agent", version: "0.1.0", systemPrompt: "You are a helpful assistant for {{organizationName}}. Current time: {{currentTime}}.", model: { provider: "anthropic", name: "claude-sonnet-4", }, tools: ["entity.query", "event.emit"], }) ``` This defines an agent that: - Uses Claude Sonnet 4 as its LLM - Has access to query entities and emit events - Receives the organization name and current time in its system prompt via template variables ## Define an Entity Type Create a file at `entity-types/customer.ts`: ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ name: "Customer", slug: "customer", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string", format: "email" }, plan: { type: "string", enum: ["free", "pro", "enterprise"] }, }, required: ["name", "email"], }, searchFields: ["name", "email"], }) ``` ## Define a Role Create a file at `roles/support.ts`: ```typescript import { defineRole } from 'struere' export default defineRole({ name: "support", description: "Support agents with read access to customers", policies: [ { resource: "customer", actions: ["list", "read"], effect: "allow" }, { resource: "customer", actions: ["delete"], effect: "deny" }, ], }) ``` ## Start Development Run the dev command to sync your definitions to the Convex backend: ```bash npx struere dev ``` You should see output like: ``` ✓ Logged in as you@example.com ✓ Loaded 1 agent, 1 entity type, 1 role, 0 triggers ✓ Synced to development environment Watching for changes... ``` The `dev` command will: 1. Auto-login if you are not authenticated (opens a browser) 2. Load all resource definitions from `agents/`, `entity-types/`, `roles/`, `triggers/`, and `tools/` 3. Sync everything to your Convex backend in the **development** environment 4. Watch for file changes and re-sync automatically Every time you save a file, the CLI re-syncs your changes. ## Test Your Agent Once synced, you can interact with your agent in two ways: ### Via the Dashboard 1. Open [app.struere.dev](https://app.struere.dev) and navigate to **Agents** 2. Select your agent ("My First Agent") 3. Use the built-in chat interface to send a message ### Via the API First, create an API key in the dashboard under **Settings > API Keys**. Select the **development** environment. Then send a request using the slug-based endpoint: ```bash curl -X POST https://your-deployment.convex.site/v1/agents/my-first-agent/chat \ -H "Authorization: Bearer sk_dev_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "Hello, what can you do?"}' ``` You should receive a JSON response: ```json { "threadId": "jd7abc123...", "message": "Hello! I'm your assistant for Acme Corp. I can query your data and log events. How can I help?", "usage": { "inputTokens": 245, "outputTokens": 32, "totalTokens": 277 } } ``` Your Convex deployment URL is shown in the dashboard under **Settings** or in your Convex dashboard at [dashboard.convex.dev](https://dashboard.convex.dev). ## Deploy to Production When you are ready to go live, deploy your agents to the production environment: ```bash npx struere deploy ``` This promotes all agent configurations to the production environment where they are accessible via production API keys (prefixed `sk_prod_`). ## Next Steps - [CLI Overview](./cli/overview) — Learn all available CLI commands - [Agent Configuration](./sdk/define-agent) — Configure models, tools, and system prompts - [Entity Types](./sdk/define-entity-type) — Define structured data schemas - [Roles & Permissions](./sdk/define-role) — Set up access control - [Triggers](./sdk/define-trigger) — Build event-driven automations --- # Platform Concepts --- ## Entities > Domain data with permission-aware CRUD operations Entities are the primary data model in Struere. They represent domain objects (teachers, students, sessions, payments) with typed schemas, environment isolation, full-text search, and permission-aware CRUD operations. ## Entity Types Entity types define the schema for a category of entities. They are created using the `defineEntityType` SDK function and synced to the platform. Each entity type specifies: - **Schema**: JSON Schema defining the data structure - **Search fields**: Fields indexed for text search - **Display config**: How entities appear in the dashboard Entity types are scoped per environment using the `by_org_env_slug` index, so development and production can have different schemas. ### Entity Type Schema in the Database | Field | Type | Description | |-------|------|-------------| | `organizationId` | ID | Owning organization | | `environment` | enum | `"development"` or `"production"` | | `name` | string | Display name | | `slug` | string | Unique identifier within org + environment | | `schema` | object | JSON Schema definition | | `searchFields` | array | Fields indexed for search | | `displayConfig` | object | Dashboard display options | | `boundToRole` | string | Role binding for user linking | | `userIdField` | string | Field storing Clerk user ID | ## Entities Entities are instances of an entity type. Each entity stores its data as a flexible JSON object that conforms to the entity type's schema. ### Entity Schema in the Database | Field | Type | Description | |-------|------|-------------| | `organizationId` | ID | Owning organization | | `environment` | enum | `"development"` or `"production"` | | `entityTypeId` | ID | Reference to entity type | | `data` | object | Entity data conforming to the type's JSON Schema | | `status` | string | Lifecycle status (e.g., `"active"`, `"deleted"`) | | `createdAt` | number | Creation timestamp | | `updatedAt` | number | Last update timestamp | ### Environment Scoping All entity queries are scoped to the current environment using the `by_org_env_type` and `by_org_env_type_status` indexes. An agent running in the development environment cannot access production entities. ## CRUD Operations All entity operations are permission-aware, flowing through the permission engine before executing. ### Create ```typescript entity.create({ type: "session", data: { teacherId: "ent_abc123", studentId: "ent_def456", guardianId: "ent_ghi789", startTime: 1700000000000, duration: 60, subject: "Mathematics", status: "scheduled", }, }) ``` Creates an entity and emits an event. The actor must have `create` permission on the entity type. ### Read ```typescript entity.get({ id: "ent_abc123" }) ``` Retrieves a single entity by ID. The response is filtered through field masks based on the actor's role, hiding or redacting fields the actor should not see. ### Query ```typescript entity.query({ type: "session", filters: { "data.status": "scheduled" }, limit: 10, }) ``` Queries entities by type with optional filters. Scope rules are applied automatically, so a teacher only sees their own sessions and a guardian only sees sessions for their children. ### Update ```typescript entity.update({ id: "ent_abc123", data: { status: "completed", teacherReport: "Great progress in algebra." }, }) ``` Updates entity data and emits an event. The actor must have `update` permission. ### Delete ```typescript entity.delete({ id: "ent_abc123" }) ``` Performs a soft delete (sets status to `"deleted"`) and emits an event. The actor must have `delete` permission. ## Relations Entities can be linked to each other through the `entityRelations` table. Relations are typed and directional. ### Link ```typescript entity.link({ fromEntityId: "ent_abc123", toEntityId: "ent_def456", relationType: "enrolled_in", }) ``` ### Unlink ```typescript entity.unlink({ fromEntityId: "ent_abc123", toEntityId: "ent_def456", relationType: "enrolled_in", }) ``` ### Environment Filtering The `entityRelations` table indexes (`by_from` and `by_to`) do not include the environment field. All relation queries apply a post-index filter to ensure environment isolation: ```typescript .filter((q) => q.eq(q.field("environment"), environment)) ``` ## Search Entity types define `searchFields` that are indexed for text search. The `entity.query` tool supports searching across these fields: ```typescript entity.query({ type: "teacher", filters: { search: "mathematics" }, limit: 5, }) ``` This searches across all fields listed in the entity type's `searchFields` array. ## Permission Flow for Entities Every entity operation passes through the full permission pipeline: ``` Actor makes request │ ▼ 1. Permission Check Does the actor's role have a policy allowing this action on this entity type? (deny overrides allow) │ ▼ 2. Scope Rules (row-level) Filter query results to only entities the actor is allowed to see (e.g., teacher sees own sessions) │ ▼ 3. Field Masks (column-level) Hide or redact fields the actor should not access (e.g., teacher cannot see paymentId) │ ▼ Filtered response returned ``` ## Tutoring Domain Example The tutoring pack defines 6 entity types that demonstrate the full entity system: | Entity Type | Key Fields | Relationships | |-------------|------------|---------------| | `teacher` | name, email, subjects, availability, hourlyRate, userId | Linked to sessions | | `student` | name, grade, subjects, notes, guardianId, preferredTeacherId | Linked to guardian | | `guardian` | name, email, phone, whatsappNumber, billingAddress, userId | Parent of students | | `session` | teacherId, studentId, guardianId, startTime, duration, status | Links teacher, student, guardian | | `payment` | guardianId, amount, status, providerReference, sessionId | Linked to session | | `entitlement` | guardianId, studentId, totalCredits, remainingCredits, expiresAt | Credits for sessions | ### Session Lifecycle Sessions follow a defined state machine: ``` pending_payment ──[payment.success]──► scheduled │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼ cancelled in_progress no_show │ ▼ completed ``` ### Scheduling Constraints - 24-hour minimum booking lead time - 2-hour reschedule cutoff - Teacher availability validation - No double booking - Credit consumption on session completion --- ## Permissions > Role-based access control with row and column security Struere implements a comprehensive permission engine that provides role-based access control (RBAC) with row-level security (scope rules) and column-level security (field masks). Every data operation in the platform passes through this engine. ## Architecture The permission engine lives in `platform/convex/lib/permissions/` and consists of five modules: | Module | File | Responsibility | |--------|------|----------------| | Context | `context.ts` | Build ActorContext with eager role resolution | | Evaluate | `evaluate.ts` | Policy evaluation with deny-overrides-allow | | Scope | `scope.ts` | Row-level security via scope filters | | Mask | `mask.ts` | Column-level security via field masks | | Tools | `tools.ts` | Tool permission checking and identity modes | ## ActorContext Every request begins by building an `ActorContext` that captures who is making the request and what they are allowed to do: ```typescript interface ActorContext { organizationId: Id<"organizations"> actorType: "user" | "agent" | "system" | "webhook" actorId: string roleIds: Id<"roles">[] isOrgAdmin?: boolean environment: "development" | "production" } ``` | Field | Description | |-------|-------------| | `organizationId` | The organization boundary for all data access | | `actorType` | Whether the caller is a user, agent, system process, or webhook | | `actorId` | Unique identifier for the specific actor | | `roleIds` | Pre-resolved role IDs (eager resolution at request start) | | `isOrgAdmin` | Whether the actor has organization admin privileges | | `environment` | Data environment scope (development or production) | ### Eager Resolution Roles are resolved once when the `ActorContext` is built, not on each permission check. This means the roles are fetched from the database at the start of the request and cached in the context object for the duration of that request. ### Context Builders | Function | Use Case | |----------|----------| | `buildActorContext()` | For authenticated user requests | | `buildSystemActorContext()` | For system operations (triggers, webhooks) | | `buildActorContextForAgent()` | For agent execution with environment from API key | ## Permission Flow Every data operation passes through a four-stage pipeline: ``` Request arrives │ ▼ ┌──────────────────────────────────────┐ │ Stage 1: Build ActorContext │ │ │ │ Resolve organization, actor type, │ │ actor ID, environment, and role IDs │ │ (eager resolution). │ │ │ │ System actors are automatically │ │ allowed through all checks. │ └──────────────┬───────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ Stage 2: Policy Evaluation │ │ │ │ Find all policies matching the │ │ requested resource and action. │ │ │ │ Deny overrides allow: │ │ - Any deny policy → access denied │ │ - At least one allow → proceed │ │ - No matching policies → denied │ └──────────────┬───────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ Stage 3: Scope Rules (Row-Level) │ │ │ │ Apply scope filters to restrict │ │ which entities are visible. │ │ │ │ Example: Teacher sees only sessions │ │ where data.teacherId matches their │ │ user ID. │ └──────────────┬───────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ Stage 4: Field Masks (Column-Level) │ │ │ │ Hide or redact specific fields │ │ from the response. │ │ │ │ Example: Teacher cannot see │ │ data.paymentId on sessions. │ └──────────────┬───────────────────────┘ │ ▼ Filtered response ``` ## Policy Evaluation Policies define what actions a role can perform on which resources. ### Action Types The platform supports 5 action types: | Action | Description | |--------|-------------| | `create` | Create a new entity | | `read` | Retrieve a single entity by ID | | `update` | Modify an existing entity | | `delete` | Soft-delete an entity | | `list` | Query multiple entities | ### Evaluation Rules 1. **Collect** all policies from the actor's roles that match the requested resource and action 2. **Deny overrides**: If any matching policy has `effect: "deny"`, access is denied regardless of allow policies 3. **Allow required**: At least one matching policy must have `effect: "allow"` for access to be granted 4. **No match = denied**: If no policies match the resource and action, access is denied ### API ```typescript canPerform(ctx, actorContext, resource, action) ``` Returns a `PermissionResult`: ```typescript interface PermissionResult { allowed: boolean reason?: string matchedPolicy?: Id<"policies"> evaluatedPolicies?: number } ``` ```typescript assertCanPerform(ctx, actorContext, resource, action) ``` Throws a `PermissionError` if access is denied. Used in mutations where denial should halt execution. ## Scope Rules (Row-Level Security) Scope rules restrict which entities an actor can see by filtering query results based on entity data fields. ### How Scope Rules Work When an actor queries entities, scope rules for their role and the target entity type are collected. These rules generate filters that are applied to the query: ``` Actor queries "session" entities │ ▼ Scope rules for actor's roles + "session" entity type collected │ ▼ Filters generated: { field: "data.teacherId", operator: "eq", value: } │ ▼ Query results filtered to only matching entities ``` ### Dynamic Value Resolution Scope rules support `actor.userId` as a dynamic value that resolves to the current actor's user ID at query time. This enables rules like "a teacher can only see sessions assigned to them": ```typescript { entityType: "session", field: "data.teacherId", operator: "eq", value: "actor.userId" } ``` ### Operators | Operator | Description | |----------|-------------| | `eq` | Field equals value | | `neq` | Field does not equal value | | `in` | Field is contained in value | | `contains` | Field contains value | ## Field Masks (Column-Level Security) Field masks control which fields an actor can see on an entity, implementing column-level security. ### Allowlist Strategy Field masks use an **allowlist strategy**: new fields added to an entity type are hidden by default until explicitly allowed in a role's field mask configuration. This is a fail-safe design that prevents accidental data exposure. ### Mask Types | Type | Behavior | |------|----------| | `hide` | Removes the field entirely from the response | | `redact` | Replaces the field value while keeping the key present | ### Example A teacher role with field masks: ```typescript fieldMasks: [ { entityType: "session", fieldPath: "data.paymentId", maskType: "hide" }, { entityType: "student", fieldPath: "data.guardianId", maskType: "hide" }, ] ``` When a teacher queries a session, the `paymentId` field is not present in the response. ## Tool Permissions The permission engine also controls which tools an agent or user can invoke. ### Tool Identity Modes | Mode | Behavior | |------|----------| | `inherit` | Tool runs with the caller's permissions | | `system` | Tool runs with system-level permissions (environment-aware, `isOrgAdmin: true`) | | `configured` | Tool runs with explicitly configured permissions | ### Permission Check ```typescript canUseTool(ctx, actorContext, toolName) ``` Checks if the actor is allowed to use the specified tool based on their role's tool permissions. ## Security Properties The permission engine guarantees the following security properties: | Property | Description | |----------|-------------| | No privileged data paths | Templates, tools, and triggers all go through the permission engine | | Defense in depth | Organization boundary checked at multiple layers | | Environment isolation | All queries, roles, configs, and entities scoped to environment | | Deny-safe | Any deny policy blocks access, regardless of allow policies | | Fail-safe | New fields hidden by default via allowlist field masking | | Audit trail | Events capture actor context for all mutations | ## System Actor The system actor bypasses all permission checks. It is used for: - Trigger execution (automated workflows need full data access) - Webhook processing (inbound messages need to create entities) - Internal operations (migrations, system maintenance) The system actor is built using `buildSystemActorContext()` and always includes the environment. --- ## Agents > AI agent configuration and execution Agents are the core execution units of the Struere platform. Each agent is an AI-powered entity with a system prompt, model configuration, and a set of tools it can use to interact with your domain data. ## Architecture Agent data is split across two tables with different scoping rules: ### agents Table (Shared) The `agents` table stores identity information that is shared across environments: | Field | Type | Description | |-------|------|-------------| | `organizationId` | ID | Organization that owns this agent | | `name` | string | Display name | | `slug` | string | URL-safe identifier for API routing | | `description` | string | Human-readable description | | `status` | enum | `"active"`, `"paused"`, or `"deleted"` | The `slug` is used for API access via `/v1/agents/:slug/chat`. ### agentConfigs Table (Environment-Scoped) The `agentConfigs` table stores the actual configuration and is scoped per environment using the `by_agent_env` index: | Field | Type | Description | |-------|------|-------------| | `agentId` | ID | Reference to the agents table | | `environment` | enum | `"development"` or `"production"` | | `version` | string | Semantic version | | `name` | string | Config display name | | `systemPrompt` | string | Compiled system prompt | | `model` | object | Provider, model name, temperature, maxTokens | | `tools` | array | Tool definitions with name, description, parameters, handlerCode, isBuiltin | | `deployedBy` | ID | User who deployed this config | This split means an agent can have different configurations in development and production. The `struere dev` command syncs to the development config, and `struere deploy` promotes configs to production. ## Execution Flow When a chat request arrives, the agent executes through this pipeline: ``` POST /v1/chat or POST /v1/agents/:slug/chat │ ▼ ┌─────────────────────────────┐ │ 1. Authentication │ │ Extract Bearer token │ │ Validate API key │ │ (SHA-256 hash lookup) │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 2. Load Agent │ │ Resolve agent by ID/slug │ │ Load config via │ │ by_agent_env index │ │ (env from API key) │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 3. Build ActorContext │ │ organizationId │ │ actorType (user/agent) │ │ environment │ │ roleIds (resolved) │ │ isOrgAdmin │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 4. Prepare Thread │ │ Get or create thread │ │ (env-scoped) │ │ Load message history │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 5. Process System Prompt │ │ Resolve {{variables}} │ │ Execute embedded queries │ │ (permission-aware) │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 6. LLM Loop (max 10 iter) │ │ Call LLM API │ │ ├─ Text response → done │ │ └─ Tool calls: │ │ Check permission │ │ Execute tool │ │ Add result to context │ │ Continue loop │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 7. Persist & Respond │ │ Append messages to thread│ │ Record execution metrics │ │ Return response │ └─────────────────────────────┘ ``` ### LLM Loop The agent runs an iterative loop with a maximum of 10 iterations. Each iteration: 1. Sends the full message history (system prompt + conversation + tool results) to the LLM 2. If the LLM responds with text only, the loop exits 3. If the LLM makes tool calls, each tool is: - Permission-checked via `canUseTool` - Executed (built-in tools run as Convex mutations; custom tools run on the tool executor service) - Results are appended to the message history 4. The loop continues with the updated history ### Tool Execution **Built-in tools** run as Convex mutations with full permission checking: | Tool | Convex Function | Description | |------|-----------------|-------------| | `entity.create` | `tools.entities.entityCreate` | Create entity + emit event | | `entity.get` | `tools.entities.entityGet` | Get entity (field-masked) | | `entity.query` | `tools.entities.entityQuery` | Query with scope filters | | `entity.update` | `tools.entities.entityUpdate` | Update + emit event | | `entity.delete` | `tools.entities.entityDelete` | Soft delete + emit event | | `entity.link` | `tools.entities.entityLink` | Create relation | | `entity.unlink` | `tools.entities.entityUnlink` | Remove relation | | `event.emit` | `tools.events.eventEmit` | Emit custom event | | `event.query` | `tools.events.eventQuery` | Query events (visibility filtered) | | `agent.chat` | `tools.agents.agentChat` | Delegate to another agent | **Custom tools** are sent to the tool executor service at `tool-executor.struere.dev` for sandboxed execution with actor context. ## Multi-Agent Communication The `agent.chat` tool enables agents to delegate work to other agents within the same organization and environment. ``` Caller Agent │ ├─ tool_call: agent.chat({ agent: "analyst", message: "..." }) │ ▼ Target Agent Resolution │ ├─ Find agent by slug ├─ Create child thread (shared conversationId) │ ▼ Target Agent Execution │ ├─ Full LLM loop with target's own config/tools/permissions │ ▼ Response returned as tool result to Caller Agent ``` ### Safety Mechanisms | Mechanism | Description | |-----------|-------------| | Depth limit | Maximum chain depth of 3 (`MAX_AGENT_DEPTH`) | | Cycle detection | Target slug checked against caller slug | | Iteration cap | Each agent limited to 10 LLM iterations independently | | Action timeout | Convex built-in timeout prevents infinite execution | ### Thread Linking All threads in a multi-agent conversation share the same `conversationId`. Child threads store a `parentThreadId` linking back to the parent. Thread metadata includes: ```typescript { conversationId: string, parentAgentSlug: string, depth: number, parentContext: object, } ``` ## Model Pricing See [Model Configuration](../reference/model-configuration) for the full list of supported models, pricing, and configuration options. ## API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/v1/chat` | POST | Chat by agent ID (Bearer token with API key) | | `/v1/agents/:slug/chat` | POST | Chat by agent slug (Bearer token with API key) | Both endpoints require a valid API key passed as a Bearer token. The environment is determined by the API key's environment field. ### Response Format ```typescript { threadId: string, message: string, usage: { inputTokens: number, outputTokens: number, totalTokens: number, durationMs: number, } } ``` --- ## Events > Audit logging and event-driven architecture Events provide a complete audit trail of all mutations in the Struere platform. Every entity creation, update, deletion, and custom action is recorded with full actor context, enabling compliance tracking, debugging, and event-driven automation. ## Event Structure Each event captures the full context of what happened, who did it, and when: | Field | Type | Description | |-------|------|-------------| | `organizationId` | ID | Owning organization | | `environment` | enum | `"development"` or `"production"` | | `eventType` | string | Category of the event (e.g., `"session.created"`, `"session.completed"`) | | `entityId` | string | ID of the affected entity (optional for non-entity events) | | `actorType` | string | `"user"`, `"agent"`, `"system"`, or `"webhook"` | | `actorId` | string | ID of the actor who performed the action | | `payload` | object | Event-specific data | | `createdAt` | number | Unix timestamp of when the event occurred | ## Event Types Events fall into two categories: system events emitted automatically by the platform, and custom events emitted explicitly by tools or triggers. ### System Events These events are emitted automatically when entity mutations occur: | Event Type | Trigger | |------------|---------| | `{type}.created` | An entity of the given type is created (e.g., `session.created`, `teacher.created`) | | `{type}.updated` | An entity's data or status is modified | | `{type}.deleted` | An entity is soft-deleted | | `entity.linked` | A relation is created between two entities | | `entity.unlinked` | A relation is removed between two entities | | `trigger.executed` | A trigger completes successfully | | `trigger.failed` | A trigger fails during execution | ### Custom Events Custom events are emitted using the `event.emit` tool: ```typescript event.emit({ eventType: "session.reminder.sent", entityId: "ent_abc123", payload: { recipientType: "guardian", recipientId: "ent_def456", channel: "whatsapp", }, }) ``` Custom event types follow a dot-notation naming convention (e.g., `"session.completed"`, `"payment.received"`, `"credits.deducted"`). ## Event Sources Events are emitted from three mutation sources: | Source | Description | |--------|-------------| | Dashboard CRUD | User actions in the admin dashboard | | Agent tool calls | Built-in tools like `entity.create` and `event.emit` | | API mutations | External API calls via HTTP endpoints | All sources capture the actor context, so events always record who performed the action. ## Querying Events Events can be queried using the `event.query` tool: ```typescript event.query({ eventType: "session.created", entityId: "ent_abc123", since: 1700000000000, limit: 50, }) ``` ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `eventType` | string | Filter by event type | | `entityId` | string | Filter by associated entity | | `since` | number | Unix timestamp for lower bound | | `limit` | number | Maximum number of events to return | All parameters are optional. When multiple are provided, they act as AND filters. ## Visibility Filtering Event queries are **visibility-filtered** based on the actor's permissions. An actor can only see events related to entities they have permission to access. This means: - A teacher only sees events for their own sessions and assigned students - A guardian only sees events related to their children - An admin sees all events within the organization The visibility filtering applies the same scope rules used for entity queries, ensuring consistent access control across the platform. ## Environment Scoping Events are scoped to the environment where they were emitted. Development events are only visible in the development environment, and production events only in production. Events are indexed by `by_org_env_type` for efficient querying by organization, environment, and event type. ## Event Payloads Event payloads contain event-specific data. For system events, the payload typically includes the entity data at the time of the event: ### {type}.created Payload ```typescript { entityType: "session", data: { teacherId: "ent_abc123", studentId: "ent_def456", startTime: 1700000000000, status: "scheduled", }, } ``` ### {type}.updated Payload ```typescript { entityType: "session", previousData: { status: "scheduled", }, newData: { status: "completed", teacherReport: "Good progress.", }, } ``` ### Custom Event Payload Custom events can include any JSON-serializable payload: ```typescript { message: "Session reminder sent", channel: "whatsapp", templateName: "session_reminder", } ``` ## Events and Triggers Events serve as the input for the trigger system. When an entity mutation emits an event, matching triggers are activated: ``` Entity mutation occurs │ ▼ System event emitted ({type}.created, {type}.updated, etc.) │ ▼ Trigger engine checks for matching triggers │ ▼ Matching triggers scheduled for execution │ ▼ Trigger actions execute (may emit more events) ``` Triggers can also emit events via the `event.emit` tool in their action chains, creating observable side effects for other triggers or audit purposes. --- ## Triggers > Automated workflows triggered by entity changes Triggers are automated workflows that execute when entities are created, updated, or deleted. They enable event-driven architecture by running a sequence of tool calls in response to data mutations, without requiring manual intervention. ## How Triggers Work When a mutation occurs (from the dashboard, an agent tool call, or an API request), the trigger engine checks for matching triggers and schedules them for execution: ``` Entity mutation (create/update/delete) │ ▼ Trigger engine scans for matching triggers │ ├─ Match on entityType ├─ Match on action (created/updated/deleted) └─ Match on condition (optional data filter) │ ▼ Matched triggers scheduled asynchronously │ ▼ Actions execute in order (fail-fast) │ ├─ Success → trigger.executed event emitted └─ Failure → trigger.failed event emitted (retry if configured) ``` ### Execution Characteristics | Property | Behavior | |----------|----------| | Timing | Asynchronous (scheduled after the originating mutation completes) | | Actor | Runs as the **system actor** with full permissions | | Error handling | **Fail-fast** (first action failure stops the chain) | | Success event | Emits `trigger.executed` | | Failure event | Emits `trigger.failed` | | Sources | Fires from dashboard CRUD, agent tool calls, and API mutations | ## Immediate Triggers By default, triggers execute as soon as they are scheduled (immediately after the originating mutation). The actions run in sequence: ```typescript { name: "Notify on New Session", slug: "notify-on-session", on: { entityType: "session", action: "created", condition: { "data.status": "scheduled" }, }, actions: [ { tool: "entity.get", args: { id: "{{trigger.data.teacherId}}" }, as: "teacher", }, { tool: "event.emit", args: { eventType: "session.notification", entityId: "{{trigger.entityId}}", payload: { teacherName: "{{steps.teacher.data.name}}" }, }, }, ], } ``` ## Scheduled Triggers Triggers can be delayed or scheduled for a specific time using the `schedule` field: ### Delay-Based Execute after a fixed delay: ```typescript schedule: { delay: 300000, } ``` This runs the trigger 5 minutes after the entity mutation. ### Time-Based Execute at a specific time derived from entity data: ```typescript schedule: { at: "{{trigger.data.startTime}}", offset: -3600000, } ``` This schedules the trigger for 1 hour before the session's `startTime`. The `at` field supports template expressions that resolve to an ISO timestamp or Unix timestamp. ### Cancel Previous When an entity is updated multiple times, `cancelPrevious` ensures only the latest scheduled run is kept: ```typescript schedule: { at: "{{trigger.data.startTime}}", offset: -3600000, cancelPrevious: true, } ``` If a session's start time is changed, the old reminder is cancelled and a new one is scheduled. ## Trigger Runs Scheduled triggers create records in the `triggerRuns` table for status tracking. ### Status Lifecycle ``` pending ──► running ──► completed │ ▼ failed ──► running (retry) ──► completed │ ▼ dead (retries exhausted) ``` | Status | Description | |--------|-------------| | `pending` | Scheduled but not yet executed | | `running` | Currently executing actions | | `completed` | All actions finished successfully | | `failed` | An action failed (may be retried) | | `dead` | Failed and exhausted all retry attempts | ### Trigger Run Fields | Field | Type | Description | |-------|------|-------------| | `triggerId` | ID | Reference to the trigger definition | | `entityId` | string | Entity that triggered the run | | `status` | enum | Current lifecycle status | | `scheduledAt` | number | When the run is scheduled to execute | | `startedAt` | number | When execution began | | `completedAt` | number | When execution finished | | `error` | string | Error message if failed | | `attempt` | number | Current retry attempt number | | `environment` | enum | Scoped to development or production | ## Retry Configuration Failed triggers can be retried with backoff: ```typescript retry: { maxAttempts: 3, backoffMs: 5000, } ``` | Field | Type | Description | |-------|------|-------------| | `maxAttempts` | number | Maximum retry attempts (minimum 1) | | `backoffMs` | number | Base delay in milliseconds between retries | When a trigger fails: 1. If `attempt < maxAttempts`, the run is rescheduled with `backoffMs` delay 2. The status transitions from `failed` back to `pending` 3. On the next attempt, it transitions to `running` 4. If all retries are exhausted, the status becomes `dead` ## Template Variable Resolution Trigger action arguments support template variables that are resolved at execution time. ### Trigger Context | Variable | Description | |----------|-------------| | `{{trigger.entityId}}` | ID of the entity that triggered the event | | `{{trigger.entityType}}` | Entity type slug | | `{{trigger.action}}` | The action: `"created"`, `"updated"`, or `"deleted"` | | `{{trigger.data.X}}` | Field `X` from the entity's current data | | `{{trigger.previousData.X}}` | Field `X` from the entity's data before an update | ### Step References | Variable | Description | |----------|-------------| | `{{steps.NAME.X}}` | Field `X` from the result of a named step | Steps are named using the `as` field on a trigger action. Later actions can reference the result: ```typescript actions: [ { tool: "entity.get", args: { id: "{{trigger.data.guardianId}}" }, as: "guardian", }, { tool: "event.emit", args: { eventType: "notification", payload: { name: "{{steps.guardian.data.name}}" }, }, }, ] ``` ## Condition Matching The `on.condition` field filters which mutations activate the trigger. All condition fields must match for the trigger to fire: ```typescript on: { entityType: "session", action: "updated", condition: { "data.status": "completed" }, } ``` This trigger only fires when a session entity is updated and its `data.status` field equals `"completed"`. Multiple conditions act as AND filters: ```typescript condition: { "data.status": "scheduled", "data.subject": "Mathematics", } ``` ## Mutation Sources Triggers fire from all mutation sources in the platform: | Source | Example | |--------|---------| | **Dashboard CRUD** | Admin creates a session via the UI | | **Agent tool calls** | Agent uses `entity.create` to schedule a session | | **API mutations** | External system calls the HTTP API | This ensures that automated workflows execute regardless of how the mutation originated. ## Dashboard Management The dashboard provides trigger management at `/triggers`: - View all configured triggers - See trigger run history with status - View scheduled (pending) runs - Retry failed runs - Cancel pending runs --- ## Environment Isolation > Development and production data separation Struere enforces strict isolation between development and production environments. All data, permissions, and configurations are scoped to one of two environments, preventing accidental cross-environment data access. ## Environments The platform defines two environments: ```typescript type Environment = "development" | "production" ``` Every request carries an environment context that is threaded through the entire execution chain: config lookup, actor context building, data queries, event logging, and tool execution. ## Scoping Rules Resources are either scoped per environment (isolated) or shared across environments: ### Environment-Scoped Resources | Table | Description | |-------|-------------| | `entityTypes` | Schema definitions can differ between dev and prod | | `entities` | All domain data is environment-isolated | | `entityRelations` | Relations between entities | | `roles` | Role definitions and their policies | | `policies` | Access control policies | | `scopeRules` | Row-level security rules | | `fieldMasks` | Column-level security masks | | `agentConfigs` | Agent configurations (system prompt, model, tools) | | `threads` | Conversation threads | | `messages` | Chat messages | | `events` | Audit log events | | `executions` | Usage tracking records | | `triggerRuns` | Scheduled trigger execution records | | `apiKeys` | API keys carry an environment field | | `installedPacks` | Pack installations | ### Shared Resources | Table | Description | |-------|-------------| | `agents` | Agent identity (name, slug, description) is shared | | `users` | User records from Clerk | | `organizations` | Organization records | | `toolPermissions` | Tool permission configurations | The `agents` table is intentionally shared so that the same agent can have different configurations in development and production. The `agentConfigs` table stores the environment-specific configuration, looked up via the `by_agent_env` index. ## API Key Environment API keys carry an `environment` field that determines which environment the request operates in: ```typescript { organizationId: Id<"organizations">, environment: "development" | "production", keyHash: string, name: string, } ``` When a chat request arrives with a Bearer token: 1. The API key is looked up by its SHA-256 hash 2. The `environment` field is extracted from the key 3. This environment is used for the entire request chain A development API key **cannot** access production data, and vice versa. This enforcement happens at the ActorContext level, where the environment is set once and used for all subsequent operations. ## Environment Threading The environment value flows through every layer of a request: ``` API Key → environment: "development" │ ▼ ActorContext.environment = "development" │ ├─► Agent config loaded via by_agent_env index (development config) ├─► Thread created/retrieved with environment = "development" ├─► Entity queries use by_org_env_type index ├─► Scope rules loaded for development roles ├─► Field masks loaded for development roles ├─► Events logged with environment = "development" └─► Execution metrics recorded with environment = "development" ``` ## CLI and Environments The CLI commands interact with specific environments: | Command | Environment | |---------|-------------| | `struere dev` | Syncs to **development** environment | | `struere deploy` | Promotes all agents to **production** environment | During development, `struere dev` watches files and syncs changes to the development environment only. Production data and configurations are not affected until `struere deploy` is explicitly run. ## Dashboard Environment Switching The dashboard supports environment switching via a URL query parameter. The `EnvironmentContext` provider reads the current environment from the URL and passes it to all data-fetching hooks. This allows admins to view and manage both development and production data from the same interface. ## Pack Installation Packs are installed per environment using the `by_org_env_pack` index. A pack can be installed in development for testing before being installed in production. Each installation tracks its own: - Customization overrides - Upgrade history - Version state ## Migrations The platform includes backfill migrations (`platform/convex/migrations/addEnvironment.ts`) that set the `environment` field on pre-existing records. Each migration batch filters for `environment === undefined`, making them naturally idempotent. Records without an environment field are treated as needing migration. ## Best Practices **Use separate API keys for each environment.** Create a development key for testing and a production key for live traffic. Never use a development key in production systems. **Test triggers in development first.** Since triggers fire from all mutation sources, test automated workflows in development before deploying to production. **Review entity type schemas before deploying.** Development and production entity types can diverge. Use `struere status` to compare local definitions against remote state before deploying. --- # SDK --- ## SDK Overview > TypeScript SDK for defining agents, entities, roles, and triggers The Struere SDK provides a set of TypeScript definition functions for building permission-aware AI agent platforms. It follows an **organization-centric architecture** where all agents, entity types, roles, triggers, and tools are managed from a single project and synced to the Struere platform. ## Installation ```bash npm install struere ``` Initialize a new project: ```bash npx 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 six definition functions, each responsible for a specific resource type: ```typescript import { defineAgent, defineTools, defineConfig, defineEntityType, defineRole, defineTrigger } from 'struere' ``` | Function | Purpose | File Location | |----------|---------|---------------| | `defineAgent` | Create and configure AI agent definitions | `agents/*.ts` | | `defineEntityType` | Define domain data schemas | `entity-types/*.ts` | | `defineRole` | Create roles with policies, scope rules, and field masks | `roles/*.ts` | | `defineTrigger` | Define event-driven automation rules | `triggers/*.ts` | | `defineTools` | Create custom tool handlers | `tools/index.ts` | | `defineConfig` | Create framework configuration with defaults | Project root | 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, EntityTypeConfig, JSONSchema, JSONSchemaProperty, RoleConfig, PolicyConfig, ScopeRuleConfig, FieldMaskConfig, TriggerConfig, TriggerAction, ToolReference, ToolParameters, ParameterDefinition, ToolHandler, ToolContext, FrameworkConfig, 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 entity types, roles, and tools across the organization - **Entity types** define the domain schema once and are available to all agents - **Roles** enforce access control consistently across all agents and API access - **Triggers** automate workflows that fire from any mutation source (dashboard, agents, or API) - **Tools** can be 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: [...] } ``` Resources are upserted by their `slug` (agents, entity types, triggers) or `name` (roles), so renaming a slug creates a new resource rather than updating the existing one. --- ## defineEntityType > Define entity type schemas for your domain The `defineEntityType` function creates and validates entity type schema definitions. Each entity type is defined in its own file under the `entity-types/` directory. ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ 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 entity type | | `slug` | `string` | Yes | URL-safe identifier, used in API queries and tool calls | | `schema` | `JSONSchema` | Yes | JSON Schema definition for entity data | | `searchFields` | `string[]` | No | Fields indexed for text search (defaults to `[]`) | | `displayConfig` | `object` | No | Controls how entities are displayed in the dashboard | | `boundToRole` | `string` | No | Binds this entity 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 `defineEntityType` 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 Entity 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[] } ``` 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"], }, } ``` ## Display Configuration The `displayConfig` field controls how entities 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 an entity type and a user role. When a user with the bound role logs in, they are associated with the matching entity: ```typescript export default defineEntityType({ 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"`. ## Full Examples ### Student Entity Type ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ 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 Entity Type ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ 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 Entity Type (Credits System) ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ 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 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", 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 | |-------|------|----------|-------------| | `name` | `string` | Yes | Unique role identifier | | `description` | `string` | No | Human-readable description | | `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 `[]`) | ### Validation `defineRole` throws errors if: - `name` is missing - `policies` is empty or missing - Any policy is missing `resource`, `actions`, or `effect` ## 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` | Entity type slug 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` | Entity 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) | ## 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` | Entity 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 an entity type are hidden by default until explicitly allowed. This is a fail-safe approach that prevents accidental data exposure. ## 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", 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", 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" }, ], }) ``` --- ## defineAgent > Create and configure AI agent definitions 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: { provider: "anthropic", name: "claude-sonnet-4", }, tools: [ "entity.create", "entity.query", "entity.update", "event.emit", "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 provider and model settings | | `tools` | `string[]` | No | Array of tool names (built-in and custom) | ### 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 `anthropic/claude-sonnet-4` with temperature `0.7` and maxTokens `4096`. ```typescript interface ModelConfig { provider: 'anthropic' | 'openai' | 'google' | 'custom' name: string temperature?: number maxTokens?: number apiKey?: string } ``` ```typescript export default defineAgent({ name: "Analyst", slug: "analyst", version: "1.0.0", systemPrompt: "You are a precise data analyst.", model: { provider: "anthropic", name: "claude-sonnet-4", temperature: 0.3, maxTokens: 8192, }, tools: ["entity.query", "event.query"], }) ``` For the full list of providers, models, and pricing, 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`. ```typescript tools: [ "entity.create", "entity.query", "event.emit", "send_email", ] ``` ## System Prompt Templates System prompts support `{{variable}}` syntax for dynamic context injection at runtime. Common variables include `{{organizationName}}`, `{{currentTime}}`, `{{agentName}}`, and `{{entityTypes}}`. ```typescript systemPrompt: `You are {{agentName}}, a coordinator for {{organizationName}}. Current time: {{currentTime}} Available entity types: {{entityTypes}}` ``` For the full variable reference and embedded query syntax, see [System Prompt Templates](../tools/system-prompt-templates). --- ## defineTrigger > Define event-driven automation rules The `defineTrigger` function creates event-driven automation rules that fire when entities are created, updated, or deleted. Each trigger is defined in its own file under the `triggers/` directory. ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Notify on New Session", slug: "notify-on-session", on: { entityType: "session", action: "created", condition: { "data.status": "scheduled" }, }, actions: [ { tool: "entity.get", args: { id: "{{trigger.data.teacherId}}" }, as: "teacher", }, { tool: "event.emit", args: { eventType: "session.notification", entityId: "{{trigger.entityId}}", payload: { teacher: "{{steps.teacher.data.name}}" }, }, }, ], }) ``` ## TriggerConfig | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Display name for the trigger | | `slug` | `string` | Yes | Unique identifier for the trigger | | `description` | `string` | No | Human-readable description | | `on` | `object` | Yes | Event configuration that activates the trigger | | `on.entityType` | `string` | Yes | Entity type slug to watch | | `on.action` | `'created' \| 'updated' \| 'deleted'` | Yes | Entity lifecycle event | | `on.condition` | `object` | No | Optional filter on entity data fields | | `schedule` | `TriggerSchedule` | No | Delay or schedule execution for a future time | | `retry` | `TriggerRetry` | No | Retry configuration for failed executions | | `actions` | `TriggerAction[]` | Yes | Ordered list of steps to execute (at least one required) | ### Validation `defineTrigger` throws errors if: - `name`, `slug`, or `on` is missing - `on.entityType` is missing - `on.action` is not one of `"created"`, `"updated"`, `"deleted"` - `actions` is empty or missing - Any action is missing `tool` or has non-object `args` - `schedule.delay` and `schedule.at` are both set - `schedule.delay` is not a positive number - `retry.maxAttempts` is less than 1 - `retry.backoffMs` is not a positive number ## TriggerAction Each action step executes a tool with the given arguments. ```typescript interface TriggerAction { tool: string args: Record as?: string } ``` | Field | Type | Description | |-------|------|-------------| | `tool` | `string` | Tool name to execute (built-in or custom) | | `args` | `object` | Arguments passed to the tool, supports template variables | | `as` | `string` | Optional name for referencing this step's result in later steps | Actions execute in order. If any action fails, the trigger stops (fail-fast behavior). ## Template Variables Trigger action arguments support `{{variable}}` template syntax for dynamic value resolution. ### Trigger Context Variables | Variable | Description | |----------|-------------| | `{{trigger.entityId}}` | ID of the entity that triggered the event | | `{{trigger.entityType}}` | Entity type slug | | `{{trigger.action}}` | The action that occurred (`"created"`, `"updated"`, `"deleted"`) | | `{{trigger.data.X}}` | Field `X` from the entity's current data | | `{{trigger.previousData.X}}` | Field `X` from the entity's data before the update (only for `"updated"` actions) | ### Step Reference Variables | Variable | Description | |----------|-------------| | `{{steps.NAME.X}}` | Access field `X` from the result of step named `NAME` (set via `as` field) | ### Template Example ```typescript actions: [ { tool: "entity.get", args: { id: "{{trigger.data.teacherId}}" }, as: "teacher", }, { tool: "entity.get", args: { id: "{{trigger.data.guardianId}}" }, as: "guardian", }, { tool: "event.emit", args: { eventType: "session.booked", entityId: "{{trigger.entityId}}", payload: { teacherName: "{{steps.teacher.data.name}}", guardianName: "{{steps.guardian.data.name}}", }, }, }, ] ``` ## Conditions The `on.condition` field filters which entity mutations trigger the actions. Only entities whose data matches all condition fields will activate the trigger: ```typescript on: { entityType: "session", action: "updated", condition: { "data.status": "completed" }, } ``` This trigger only fires when a session is updated and its `status` field is `"completed"`. ## Scheduled Triggers Triggers can be scheduled to run at a future time instead of executing immediately. ```typescript interface TriggerSchedule { delay?: number at?: string offset?: number cancelPrevious?: boolean } ``` | Field | Type | Description | |-------|------|-------------| | `delay` | `number` | Delay in milliseconds before execution | | `at` | `string` | Template expression resolving to an ISO timestamp or Unix timestamp | | `offset` | `number` | Offset in milliseconds to add to the `at` time (can be negative) | | `cancelPrevious` | `boolean` | Cancel any pending scheduled run for the same entity before scheduling | `delay` and `at` are mutually exclusive. ### Schedule Examples Send a reminder 1 hour before a session starts: ```typescript export default defineTrigger({ name: "Session Reminder", slug: "session-reminder", on: { entityType: "session", action: "created", condition: { "data.status": "scheduled" }, }, schedule: { at: "{{trigger.data.startTime}}", offset: -3600000, cancelPrevious: true, }, actions: [ { tool: "entity.get", args: { id: "{{trigger.data.guardianId}}" }, as: "guardian", }, { tool: "event.emit", args: { eventType: "session.reminder", entityId: "{{trigger.entityId}}", payload: { guardianName: "{{steps.guardian.data.name}}" }, }, }, ], }) ``` Delay notification by 5 minutes: ```typescript schedule: { delay: 300000, } ``` ### Trigger Runs Scheduled triggers create records in the `triggerRuns` table with status tracking: | Status | Description | |--------|-------------| | `pending` | Scheduled but not yet executed | | `running` | Currently executing | | `completed` | Successfully finished | | `failed` | Failed but may be retried | | `dead` | Failed and exhausted all retry attempts | ## Retry Configuration Failed triggers can be retried with exponential backoff. ```typescript interface TriggerRetry { maxAttempts?: number backoffMs?: number } ``` | Field | Type | Description | |-------|------|-------------| | `maxAttempts` | `number` | Maximum number of retry attempts (minimum 1) | | `backoffMs` | `number` | Base delay in milliseconds between retries | ```typescript retry: { maxAttempts: 3, backoffMs: 5000, } ``` This retries up to 3 times with 5-second base backoff between attempts. ## Execution Behavior - Triggers execute **asynchronously** (scheduled after the originating mutation completes) - Triggers run as the **system actor** with full permissions - Actions execute in **fail-fast** order (first failure stops the chain) - Successful triggers emit a `trigger.executed` event - Failed triggers emit a `trigger.failed` event - Triggers fire from **all mutation sources**: dashboard CRUD, agent tool calls, and API mutations ## Full Examples ### Notify Guardian on Session Completion ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Notify on Completion", slug: "notify-on-completion", on: { entityType: "session", action: "updated", condition: { "data.status": "completed" }, }, actions: [ { tool: "entity.get", args: { id: "{{trigger.data.guardianId}}" }, as: "guardian", }, { tool: "event.emit", args: { eventType: "session.completed", entityId: "{{trigger.entityId}}", payload: { guardianName: "{{steps.guardian.data.name}}", subject: "{{trigger.data.subject}}", }, }, }, ], }) ``` ### Auto-Deduct Credits on Completion ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Deduct Credits", slug: "deduct-credits", on: { entityType: "session", action: "updated", condition: { "data.status": "completed" }, }, retry: { maxAttempts: 3, backoffMs: 2000, }, actions: [ { tool: "entity.query", args: { type: "entitlement", filters: { "data.studentId": "{{trigger.data.studentId}}" }, limit: 1, }, as: "entitlements", }, { tool: "event.emit", args: { eventType: "credits.deducted", entityId: "{{trigger.entityId}}", payload: { studentId: "{{trigger.data.studentId}}", }, }, }, ], }) ``` ### Scheduled Follow-Up ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Post-Session Follow-Up", slug: "post-session-followup", on: { entityType: "session", action: "updated", condition: { "data.status": "completed" }, }, schedule: { delay: 86400000, }, retry: { maxAttempts: 2, backoffMs: 60000, }, actions: [ { tool: "event.emit", args: { eventType: "session.followup", entityId: "{{trigger.entityId}}", payload: { teacherId: "{{trigger.data.teacherId}}", studentId: "{{trigger.data.studentId}}", }, }, }, ], }) ``` --- ## defineTools > Create custom tool handlers for agents The `defineTools` function creates and validates custom tool handlers that agents can use alongside built-in tools. Custom tools are defined in `tools/index.ts` and are available to any agent in the organization. ```typescript import { defineTools } from 'struere' export default defineTools([ { name: "send_email", description: "Send an email to a recipient", parameters: { type: "object", properties: { to: { type: "string", description: "Recipient email address" }, subject: { type: "string", description: "Email subject line" }, body: { type: "string", description: "Email body content" }, }, required: ["to", "subject", "body"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: [{ to: [{ email: args.to }] }], from: { email: "noreply@example.com" }, subject: args.subject, content: [{ type: "text/plain", value: args.body }], }), }) return { success: response.ok } }, }, ]) ``` ## Tool Definition Each tool in the array requires the following fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Unique tool identifier, referenced in agent `tools` arrays | | `description` | `string` | Yes | Description shown to the LLM for tool selection | | `parameters` | `ToolParameters` | Yes | JSON Schema defining the tool's input parameters | | `handler` | `function` | Yes | Async function that executes when the tool is called | ### Validation `defineTools` throws errors if any tool is missing `name`, `description`, `parameters`, or `handler`. ## Handler Function The handler function receives three arguments: ```typescript handler: async (args, context, sandboxedFetch) => { return { result: "value" } } ``` | Argument | Type | Description | |----------|------|-------------| | `args` | `Record` | Parsed arguments matching the `parameters` schema | | `context` | `ExecutionContext` | Actor and organization context | | `sandboxedFetch` | `function` | Fetch function restricted to allowed domains | The handler must return a JSON-serializable value. This value is passed back to the LLM as the tool result. ### ExecutionContext ```typescript interface ExecutionContext { organizationId: string actorId: string actorType: "user" | "agent" | "system" } ``` ## Using Custom Tools in Agents Reference custom tools by their `name` in any agent's `tools` array: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Support Agent", slug: "support", version: "1.0.0", systemPrompt: "You are a customer support agent.", tools: [ "entity.query", "entity.update", "event.emit", "send_email", ], }) ``` Custom tool names and built-in tool names share the same namespace. Avoid naming conflicts by not using the `entity.`, `event.`, or `agent.` prefixes for custom tools. For the sandboxed fetch allowlist, execution environment details, and more examples, see [Custom Tools](../tools/custom-tools). --- # Tools --- ## Built-in Tools > Pre-built tools available to all agents Struere provides a set of built-in tools that agents can use to interact with entities, events, calendars, WhatsApp, and other agents. All built-in tools are **permission-aware** — every invocation builds an `ActorContext` from the calling agent's identity and evaluates policies, scope rules, and field masks before returning results. ## Tool Reference | Tool | Category | Description | |------|----------|-------------| | `entity.create` | Entity | Create a new entity of a specified type | | `entity.get` | Entity | Retrieve a single entity by ID | | `entity.query` | Entity | Query entities by type with optional filters | | `entity.update` | Entity | Update an existing entity's data | | `entity.delete` | Entity | Soft-delete an entity | | `entity.link` | Entity | Create a relation between two entities | | `entity.unlink` | Entity | Remove a relation between two entities | | `event.emit` | Event | Emit a custom event for audit logging | | `event.query` | Event | Query historical events with filters | | `calendar.list` | Calendar | List calendar events for a user | | `calendar.create` | Calendar | Create a calendar event | | `calendar.update` | Calendar | Update a calendar event | | `calendar.delete` | Calendar | Delete a calendar event | | `calendar.freeBusy` | Calendar | Check free/busy availability | | `whatsapp.send` | WhatsApp | Send a WhatsApp message | | `whatsapp.getConversation` | WhatsApp | Get conversation history | | `whatsapp.getStatus` | WhatsApp | Check WhatsApp connection status | | `agent.chat` | Agent | Send a message to another agent and get its response | ## Enabling Tools Specify which tools an agent can use in its definition: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Support Agent", slug: "support", version: "0.1.0", systemPrompt: "You help customers with their requests.", model: { provider: "anthropic", name: "claude-sonnet-4" }, tools: [ "entity.create", "entity.query", "entity.update", "event.emit", "agent.chat", ], }) ``` ## Permission Enforcement Every tool call goes through the full permission pipeline: 1. **ActorContext resolution** — The agent's roles are eagerly resolved for its environment 2. **Policy evaluation** — `assertCanPerform()` checks if the actor has a matching allow policy (deny overrides allow) 3. **Scope filtering** — Row-level security filters results to only records the actor can access 4. **Field masking** — Column-level security strips unauthorized fields from responses If a permission check fails, the tool returns an error to the agent, which can then inform the user. ## Entity Tools ### entity.create Creates a new entity of a specified type. Emits a `{type}.created` event and fires any matching triggers. **Parameters:** ```typescript { type: string data: object status?: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `type` | `string` | Yes | The entity type slug (e.g., `"teacher"`, `"student"`) | | `data` | `object` | Yes | The entity's data fields, matching the entity type schema | | `status` | `string` | No | Initial status. Defaults to `"active"` | **Returns:** ```typescript { id: string } ``` **Example agent usage:** The agent receives a request to create a new student and calls `entity.create` with the appropriate data: ```json { "type": "student", "data": { "name": "Alice Johnson", "grade": "10th", "subjects": ["math", "physics"] } } ``` --- ### entity.get Retrieves a single entity by its ID. The response is filtered through scope rules and field masks. **Parameters:** ```typescript { id: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | `string` | Yes | The entity ID to retrieve | **Returns:** ```typescript { id: string type: string status: string data: object createdAt: number updatedAt: number } ``` The `data` field will have hidden fields removed based on the actor's field masks. If the entity is outside the actor's scope, a permission error is thrown. --- ### entity.query Queries entities by type with optional filters. Results are scope-filtered and field-masked. **Parameters:** ```typescript { type: string filters?: object status?: string limit?: number } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `type` | `string` | Yes | The entity type slug to query | | `filters` | `object` | No | Key-value filters applied to entity `data` fields | | `status` | `string` | No | Filter by entity status | | `limit` | `number` | No | Maximum number of results. Defaults to `100` | **Filter operators:** Filters support both exact match and operator-based filtering: ```json { "type": "session", "filters": { "subject": "math", "grade": { "_op_in": ["9th", "10th", "11th"] }, "hourlyRate": { "_op_gte": 50, "_op_lte": 100 } }, "status": "active", "limit": 25 } ``` Available operators: | Operator | Description | |----------|-------------| | `_op_in` | Value is in the provided array | | `_op_nin` | Value is not in the provided array | | `_op_ne` | Value is not equal to | | `_op_gt` | Greater than (numeric) | | `_op_gte` | Greater than or equal (numeric) | | `_op_lt` | Less than (numeric) | | `_op_lte` | Less than or equal (numeric) | **Returns:** ```typescript Array<{ id: string type: string status: string data: object createdAt: number updatedAt: number }> ``` --- ### entity.update Updates an existing entity's data fields. The update is merged with existing data. Emits a `{type}.updated` event and fires matching triggers. **Parameters:** ```typescript { id: string type?: string data: object status?: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | `string` | Yes | The entity ID to update | | `type` | `string` | No | Entity type slug for validation. If provided, the update will fail if the entity is not of this type. | | `data` | `object` | Yes | Fields to update (merged with existing data) | | `status` | `string` | No | New status value | Field masks are applied to the update — the actor can only modify fields their role permits. Fields outside the actor's mask are silently ignored. **Returns:** ```typescript { success: boolean } ``` --- ### entity.delete Soft-deletes an entity by setting its status to `"deleted"` and recording a `deletedAt` timestamp. Emits a `{type}.deleted` event and fires matching triggers. **Parameters:** ```typescript { id: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | `string` | Yes | The entity ID to delete | **Returns:** ```typescript { success: boolean } ``` --- ### entity.link Creates a typed relation between two entities. Requires `update` permission on the source entity and `read` permission on the target entity. Emits an `entity.linked` event. **Parameters:** ```typescript { fromId: string toId: string relationType: string metadata?: object } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `fromId` | `string` | Yes | The source entity ID | | `toId` | `string` | Yes | The target entity ID | | `relationType` | `string` | Yes | The relation type label (e.g., `"teaches"`, `"guardian_of"`) | | `metadata` | `object` | No | Arbitrary metadata to attach to the relation | If the relation already exists, the existing relation ID is returned with `existing: true`. **Returns:** ```typescript { id: string existing: boolean } ``` --- ### entity.unlink Removes a relation between two entities. Requires `update` permission on the source entity and `read` permission on the target entity. Emits an `entity.unlinked` event. **Parameters:** ```typescript { fromId: string toId: string relationType: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `fromId` | `string` | Yes | The source entity ID | | `toId` | `string` | Yes | The target entity ID | | `relationType` | `string` | Yes | The relation type to remove | **Returns:** ```typescript { success: boolean } ``` ## Event Tools ### event.emit Emits a custom event for audit logging and tracking. Events are scoped to the current environment. **Parameters:** ```typescript { eventType: string entityId?: string entityTypeSlug?: string payload?: object } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `eventType` | `string` | Yes | The event type identifier (e.g., `"session.notification"`, `"payment.reminder"`) | | `entityId` | `string` | No | The related entity ID, if applicable | | `entityTypeSlug` | `string` | No | The entity type slug, used for visibility filtering | | `payload` | `object` | No | Arbitrary event data | **Returns:** ```typescript { id: string } ``` --- ### event.query Queries historical events with optional filters. Results are visibility-filtered based on the actor's permissions on related entities. **Parameters:** ```typescript { eventType?: string entityId?: string entityTypeSlug?: string since?: number limit?: number } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `eventType` | `string` | No | Filter by event type | | `entityId` | `string` | No | Filter by related entity ID | | `entityTypeSlug` | `string` | No | Filter by entity type slug | | `since` | `number` | No | Unix timestamp in milliseconds; only return events after this time | | `limit` | `number` | No | Maximum number of results. Defaults to `50` | When an `entityId` is specified, the actor must have `read` permission on that entity and the entity must be within the actor's scope. When querying by `eventType` or without filters, events associated with entities outside the actor's scope are automatically excluded. **Returns:** ```typescript Array<{ _id: string eventType: string entityId?: string entityTypeSlug?: string actorId: string actorType: string payload: object timestamp: number }> ``` ## Calendar Tools Calendar tools integrate with Google Calendar to manage events for users in your organization. Requires a Google Calendar integration to be configured. ### calendar.list Lists calendar events for a user within a time range. **Parameters:** ```typescript { userId: string timeMin: string timeMax: string maxResults?: number } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `userId` | `string` | Yes | The user ID whose calendar to query | | `timeMin` | `string` | Yes | Start of time range (ISO 8601 format) | | `timeMax` | `string` | Yes | End of time range (ISO 8601 format) | | `maxResults` | `number` | No | Maximum events to return | --- ### calendar.create Creates a new calendar event for a user. **Parameters:** ```typescript { userId: string summary: string startTime: string endTime?: string durationMinutes?: number description?: string attendees?: string[] timeZone?: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `userId` | `string` | Yes | The user ID whose calendar to create the event on | | `summary` | `string` | Yes | Event title | | `startTime` | `string` | Yes | Event start time (ISO 8601 format) | | `endTime` | `string` | No | Event end time (ISO 8601 format). Provide either `endTime` or `durationMinutes`. | | `durationMinutes` | `number` | No | Duration in minutes. Used to calculate `endTime` if not provided. | | `description` | `string` | No | Event description | | `attendees` | `string[]` | No | List of attendee email addresses | | `timeZone` | `string` | No | IANA timezone (e.g., `"America/Santiago"`) | --- ### calendar.update Updates an existing calendar event. **Parameters:** ```typescript { userId: string eventId: string summary?: string startTime?: string endTime?: string description?: string attendees?: string[] status?: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `userId` | `string` | Yes | The user ID who owns the calendar event | | `eventId` | `string` | Yes | The calendar event ID to update | | `summary` | `string` | No | New event title | | `startTime` | `string` | No | New start time (ISO 8601 format) | | `endTime` | `string` | No | New end time (ISO 8601 format) | | `description` | `string` | No | New description | | `attendees` | `string[]` | No | Updated attendee list | | `status` | `string` | No | Event status (e.g., `"cancelled"`) | --- ### calendar.delete Deletes a calendar event. **Parameters:** ```typescript { userId: string eventId: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `userId` | `string` | Yes | The user ID who owns the calendar event | | `eventId` | `string` | Yes | The calendar event ID to delete | --- ### calendar.freeBusy Checks a user's free/busy availability within a time range. **Parameters:** ```typescript { userId: string timeMin: string timeMax: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `userId` | `string` | Yes | The user ID to check availability for | | `timeMin` | `string` | Yes | Start of time range (ISO 8601 format) | | `timeMax` | `string` | Yes | End of time range (ISO 8601 format) | ## WhatsApp Tools WhatsApp tools allow agents to send messages and retrieve conversation history. Requires a WhatsApp integration to be configured for the organization. ### whatsapp.send Sends a text message to a WhatsApp number. **Parameters:** ```typescript { to: string text: string } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `to` | `string` | Yes | Recipient phone number (E.164 format, e.g., `"+56912345678"`) | | `text` | `string` | Yes | Message text to send | **Returns:** ```typescript { messageId: string to: string status: "sent" } ``` --- ### whatsapp.getConversation Retrieves the message history with a specific phone number. **Parameters:** ```typescript { phoneNumber: string limit?: number } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `phoneNumber` | `string` | Yes | Phone number to get conversation for | | `limit` | `number` | No | Maximum messages to return | --- ### whatsapp.getStatus Checks the WhatsApp connection status for the current organization. **Parameters:** No parameters required. **Returns:** ```typescript { connected: boolean status: string phoneNumber?: string lastConnectedAt?: number } ``` ## Agent Tools ### agent.chat Sends a message to another agent within the same organization and environment, and returns its response. This enables multi-agent workflows where a coordinator agent can delegate specialized tasks to other agents. **Parameters:** ```typescript { agent: string message: string context?: object } ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `agent` | `string` | Yes | The target agent's slug | | `message` | `string` | Yes | The message to send to the target agent | | `context` | `object` | No | Additional context passed to the target agent's thread metadata | **Returns:** ```typescript { response: string threadId: string agentSlug: string usage: { inputTokens: number outputTokens: number totalTokens: number } } ``` ### Safety Mechanisms The `agent.chat` tool includes several protections against runaway execution: | Mechanism | Behavior | |-----------|----------| | **Depth limit** | Maximum delegation depth of **3**. If agent A calls agent B which calls agent C which calls agent D, agent D's call will be rejected. | | **Cycle detection** | An agent cannot call itself. If agent A tries to invoke `agent.chat` with its own slug, the call is rejected immediately. | | **Iteration cap** | Each agent in the chain has an independent limit of **10** LLM loop iterations, preventing any single agent from running indefinitely. | | **Action timeout** | Convex's built-in action timeout applies to the entire chain, providing an upper bound on total execution time. | ### Thread Linking All threads created during a multi-agent conversation share the same `conversationId`. Child threads store a `parentThreadId` linking back to the parent thread. Thread metadata includes: ```typescript { conversationId: string parentAgentSlug: string depth: number parentContext: object } ``` ### Multi-Agent Example A coordinator agent that delegates to specialized agents: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Coordinator", slug: "coordinator", version: "0.1.0", systemPrompt: `You are a coordinator that routes requests to specialized agents. For scheduling questions, delegate to the scheduler agent. For billing questions, delegate to the billing agent. For general questions, answer directly.`, model: { provider: "anthropic", name: "claude-sonnet-4" }, tools: ["entity.query", "agent.chat"], }) ``` When the coordinator receives a scheduling request, it calls: ```json { "agent": "scheduler", "message": "The guardian wants to book a math session for Tuesday at 3 PM.", "context": { "guardianId": "ent_abc123", "studentId": "ent_def456" } } ``` The scheduler agent runs its own LLM loop with its own tools and permissions, then returns its response to the coordinator. --- ## Custom Tools > Build custom tool handlers executed on the tool executor service Custom tools extend your agents' capabilities beyond the built-in entity, event, and agent tools. Tool handlers are defined in your project and executed on the tool executor service with a restricted fetch allowlist. ## Defining Custom Tools Create a `tools/index.ts` file in your project root and use `defineTools` to register your custom tools: ```typescript import { defineTools } from 'struere' export default defineTools([ { name: "send_email", description: "Send an email to a recipient", parameters: { type: "object", properties: { to: { type: "string", description: "Recipient email address" }, subject: { type: "string", description: "Email subject line" }, body: { type: "string", description: "Email body content" }, }, required: ["to", "subject", "body"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: [{ to: [{ email: args.to }] }], from: { email: "noreply@example.com" }, subject: args.subject, content: [{ type: "text/plain", value: args.body }], }), }) return { success: response.ok } }, }, ]) ``` ## Tool Definition Schema Each custom tool requires the following fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Unique tool name. Used to reference the tool in agent definitions. | | `description` | `string` | Yes | Human-readable description. Passed to the LLM to help it understand when to use the tool. | | `parameters` | `object` | Yes | JSON Schema defining the tool's input parameters. | | `handler` | `function` | Yes | Async function that executes the tool logic. | The `parameters` field follows the JSON Schema specification: ```typescript { name: "create_stripe_customer", description: "Create a new customer in Stripe", parameters: { type: "object", properties: { email: { type: "string", description: "Customer email" }, name: { type: "string", description: "Customer name" }, }, required: ["email", "name"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.stripe.com/v1/customers", { method: "POST", headers: { "Authorization": `Bearer ${process.env.STRIPE_SECRET_KEY}`, "Content-Type": "application/x-www-form-urlencoded", }, body: `email=${encodeURIComponent(args.email)}&name=${encodeURIComponent(args.name)}&metadata[orgId]=${context.organizationId}`, }) const customer = await response.json() return { customerId: customer.id, email: customer.email } }, } ``` ## Handler Function Signature ```typescript handler: (args: object, context: ExecutionContext, fetch: SandboxedFetch) => Promise ``` ### args The parsed arguments object matching the tool's `parameters` schema. The LLM generates these based on the conversation context. ### context (ExecutionContext) Provides information about the calling actor: ```typescript interface ExecutionContext { organizationId: string actorId: string actorType: "user" | "agent" | "system" } ``` | Field | Description | |-------|-------------| | `organizationId` | The Convex organization ID of the caller | | `actorId` | The ID of the user or agent making the call | | `actorType` | Whether the caller is a `"user"`, `"agent"`, or `"system"` | Use the context to scope your tool's behavior to the current organization and actor: ```typescript handler: async (args, context, fetch) => { const response = await fetch("https://api.stripe.com/v1/customers", { method: "POST", headers: { "Authorization": `Bearer ${process.env.STRIPE_SECRET_KEY}`, "Content-Type": "application/x-www-form-urlencoded", }, body: `email=${args.email}&metadata[orgId]=${context.organizationId}`, }) return await response.json() } ``` ### fetch (SandboxedFetch) A restricted version of the standard `fetch` API that only allows requests to approved domains. Attempts to call domains outside the allowlist will throw an error. ## Sandboxed Fetch Allowlist The following domains are permitted for outbound requests from custom tool handlers: | Domain | Typical Use | |--------|-------------| | `api.openai.com` | OpenAI API calls | | `api.anthropic.com` | Anthropic API calls | | `api.stripe.com` | Payment processing | | `api.sendgrid.com` | Email delivery | | `api.twilio.com` | SMS and voice | | `hooks.slack.com` | Slack webhook notifications | | `discord.com` | Discord webhook notifications | | `api.github.com` | GitHub API integration | Any `fetch` call to a domain not on this list will be rejected with an error. ## Execution Environment Custom tool handlers execute on the tool executor service at `tool-executor.struere.dev`. The execution flow is: ``` Agent LLM decides to use custom tool | v Convex backend receives tool call | v POST to tool-executor.struere.dev/execute | v Handler code executes in sandbox | v Result returned to agent LLM loop ``` The tool executor also provides a validation endpoint at `POST /validate` that checks handler code syntax before deployment. ## Using Custom Tools in Agents Reference custom tools by name in your agent definitions alongside built-in tools: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Notification Agent", slug: "notifications", version: "0.1.0", systemPrompt: "You send notifications to users via email and Slack.", model: { provider: "anthropic", name: "claude-sonnet-4" }, tools: [ "entity.query", "event.emit", "send_email", "send_slack_notification", ], }) ``` ## Complete Example A `tools/index.ts` with multiple custom tools: ```typescript import { defineTools } from 'struere' export default defineTools([ { name: "send_email", description: "Send an email to a recipient", parameters: { type: "object", properties: { to: { type: "string", description: "Recipient email address" }, subject: { type: "string", description: "Email subject line" }, body: { type: "string", description: "Email body content" }, }, required: ["to", "subject", "body"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: [{ to: [{ email: args.to }] }], from: { email: "noreply@example.com" }, subject: args.subject, content: [{ type: "text/plain", value: args.body }], }), }) return { success: response.ok } }, }, { name: "send_slack_notification", description: "Post a message to a Slack channel via webhook", parameters: { type: "object", properties: { message: { type: "string", description: "The message text to post" }, channel: { type: "string", description: "Slack channel name" }, }, required: ["message"], }, handler: async (args, context, fetch) => { const webhookUrl = process.env.SLACK_WEBHOOK_URL const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: args.message, channel: args.channel, }), }) return { success: response.ok } }, }, { name: "create_stripe_customer", description: "Create a new customer in Stripe", parameters: { type: "object", properties: { email: { type: "string", description: "Customer email" }, name: { type: "string", description: "Customer name" }, }, required: ["email", "name"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.stripe.com/v1/customers", { method: "POST", headers: { "Authorization": `Bearer ${process.env.STRIPE_SECRET_KEY}`, "Content-Type": "application/x-www-form-urlencoded", }, body: `email=${encodeURIComponent(args.email)}&name=${encodeURIComponent(args.name)}&metadata[orgId]=${context.organizationId}`, }) const customer = await response.json() return { customerId: customer.id, email: customer.email } }, }, ]) ``` --- ## System Prompt Templates > Dynamic variables and embedded queries in system prompts System prompts support a template syntax that injects dynamic data at runtime. This allows agents to receive up-to-date context about the organization, current time, available entity types, and even live query results directly in their system prompt. ## Template Syntax Templates use double curly braces: `{{variableName}}`. Variables are resolved when the agent processes a message, before the system prompt is sent to the LLM. ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Support Agent", slug: "support", version: "0.1.0", systemPrompt: `You are {{agentName}}, an assistant for {{organizationName}}. Current time: {{currentTime}} Available entity types: {{entityTypes}} Use entity.query to search for entities by type.`, model: { provider: "anthropic", name: "claude-sonnet-4" }, tools: ["entity.query", "entity.get", "event.emit"], }) ``` ## Available Variables | Variable | Type | Description | |----------|------|-------------| | `{{currentTime}}` | `string` | ISO 8601 timestamp (e.g., `"2025-03-15T14:30:00.000Z"`) | | `{{datetime}}` | `string` | ISO 8601 timestamp (alias for `currentTime`) | | `{{timestamp}}` | `number` | Unix timestamp in milliseconds | | `{{organizationName}}` | `string` | The organization's display name | | `{{organizationId}}` | `string` | The Convex organization ID | | `{{agentName}}` | `string` | The agent's display name | | `{{agent.name}}` | `string` | The agent's display name (dotted access) | | `{{agent.slug}}` | `string` | The agent's slug identifier | | `{{userId}}` | `string` | The current user's ID (if applicable) | | `{{threadId}}` | `string` | The current conversation thread ID | | `{{message}}` | `string` | The current user message being processed | | `{{thread.metadata.X}}` | `any` | Access thread metadata field `X` (replace `X` with the field name) | | `{{entityTypes}}` | `array` | JSON array of all entity types in the current environment | | `{{roles}}` | `array` | JSON array of all roles in the current environment | ### Variable Resolution Variables support dot notation for nested access. The template engine walks the context object following each dot-separated segment: - `{{agent.name}}` resolves to `context.agent.name` - `{{thread.metadata.customerId}}` resolves to `context.thread.metadata.customerId` If a variable resolves to an object or array, it is serialized as JSON. If a variable cannot be resolved, the template outputs `[TEMPLATE_ERROR: variableName not found]`. ### entityTypes Structure The `{{entityTypes}}` variable resolves to a JSON array of entity type objects: ```json [ { "name": "Teacher", "slug": "teacher", "description": "Tutors who conduct sessions", "schema": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string", "format": "email" }, "hourlyRate": { "type": "number" } } }, "searchFields": ["name", "email"] }, { "name": "Student", "slug": "student", "schema": { "type": "object", "properties": { "name": { "type": "string" }, "grade": { "type": "string" } } }, "searchFields": ["name"] } ] ``` This gives agents full awareness of the data model so they can construct valid `entity.query` and `entity.create` calls. ## Function Calls (Embedded Queries) Templates can embed live queries that execute at prompt-resolution time. Function calls use the same double curly brace syntax with parentheses: ``` {{functionName({"key": "value"})}} ``` ### entity.query Queries entities by type and injects the results into the system prompt: ``` {{entity.query({"type": "teacher", "limit": 5})}} ``` This resolves to a JSON array of entity objects, filtered through the agent's permissions (scope rules and field masks apply). ### entity.get Retrieves a single entity by type and ID: ``` {{entity.get({"type": "customer", "id": "ent_abc123"})}} ``` ### Nested Templates Function arguments can contain template variables, enabling dynamic queries based on thread context: ``` {{entity.get({"type": "customer", "id": "{{thread.metadata.customerId}}"})}} ``` In this example: 1. `{{thread.metadata.customerId}}` is resolved first to the actual customer ID 2. The resolved ID is then used as the argument to `entity.get` 3. The entity data is fetched and injected into the system prompt This is particularly useful for agents that need context about a specific entity associated with the current conversation thread. ## Unsupported Syntax Handlebars block helpers are **not supported**. The following will not work: ``` {{#each entityTypes}} - {{this.name}} {{/each}} ``` ``` {{#if userId}} User is logged in. {{/if}} ``` Instead, use the raw variable which returns the JSON representation: ``` Available types: {{entityTypes}} ``` The LLM can parse the JSON array directly. ## Result Truncation Function call results are truncated to **10 KB** to prevent excessively large system prompts. If a result exceeds this limit, it is cut off with a `...[truncated]` suffix. ## Error Handling If a function call fails, the template engine replaces it with an error marker: | Error | Output | |-------|--------| | Invalid JSON arguments | `[TEMPLATE_ERROR: entity.query - invalid JSON arguments]` | | Permission denied | `[]` (empty array) | | Tool not found | `[TEMPLATE_ERROR: toolName - tool not found]` | | Execution failure | `[TEMPLATE_ERROR: toolName - error message]` | Permission errors produce empty results rather than error messages, so agents gracefully degrade when they lack access to certain data. ## Full Example A scheduling agent with rich context injection: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Scheduler", slug: "scheduler", version: "0.1.0", systemPrompt: `You are {{agentName}}, a scheduling assistant for {{organizationName}}. Current time: {{currentTime}} ## Available Entity Types {{entityTypes}} ## Current Teachers {{entity.query({"type": "teacher"})}} ## Current Students {{entity.query({"type": "student"})}} ## Instructions - Use the entity types above to understand the data schema - Query sessions with entity.query to check for conflicts - Create new sessions with entity.create - Always verify teacher availability before booking - Sessions must be booked at least 24 hours in advance`, model: { provider: "anthropic", name: "claude-sonnet-4", temperature: 0.3, }, tools: [ "entity.create", "entity.get", "entity.query", "entity.update", "event.emit", ], }) ``` A customer-support agent that loads the customer's profile from thread metadata: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Customer Support", slug: "customer-support", version: "0.1.0", systemPrompt: `You are a support agent for {{organizationName}}. ## Customer Profile {{entity.get({"type": "customer", "id": "{{thread.metadata.customerId}}"})}} ## Recent Events {{entity.query({"type": "ticket", "filters": {"customerId": "{{thread.metadata.customerId}}"}, "limit": 10})}} Help the customer with their request. You have their profile and recent tickets loaded above.`, model: { provider: "anthropic", name: "claude-sonnet-4" }, tools: ["entity.query", "entity.update", "event.emit"], }) ``` --- # CLI --- ## CLI Overview > Command-line interface for managing Struere agents The Struere CLI is your primary interface for defining, syncing, and deploying agents and platform resources. It is built with Commander.js and ships as part of the `struere` npm package. ## Available Commands | Command | Purpose | |---------|---------| | `struere init` | Initialize an organization-centric project, scaffold directories | | `struere dev` | Watch all files, sync everything to Convex on change | | `struere deploy` | Deploy all agents to production | | `struere add ` | Scaffold a new agent, entity-type, role, trigger, or eval | | `struere entities` | Browse and manage entities interactively | | `struere status` | Compare local file definitions vs remote state | | `struere pull` | Pull remote resources to local files | | `struere login` | Browser-based OAuth authentication | | `struere logout` | Clear stored credentials | | `struere whoami` | Display the current logged-in user and organization | ## Quick Start ```bash npm install struere npx struere init npx struere dev ``` ## Auto-Run Behavior The CLI automatically handles prerequisites so you never need to run setup commands manually: - **No `struere.json`?** — Automatically runs `init` before proceeding - **Not logged in?** — Automatically runs `login` before proceeding This means running `npx struere dev` in an empty directory will walk you through project initialization and authentication before starting the development sync. ## Configuration ### Project Configuration (`struere.json`) Located at the root of your project, this file identifies your organization: ```json { "version": "2.0", "organization": { "id": "org_abc123", "slug": "acme-corp", "name": "Acme Corp" } } ``` ### Credentials (`~/.struere/credentials.json`) Authentication tokens are stored locally in your home directory. These are managed automatically by `struere login` and `struere logout`. ## Environment Variables | Variable | Default | Purpose | |----------|---------|---------| | `STRUERE_CONVEX_URL` | `your-deployment.convex.cloud` | Convex deployment URL | | `STRUERE_API_KEY` | — | API key for production deployments | | `STRUERE_AUTH_URL` | `app.struere.dev` | Auth callback URL for OAuth flow | ## Architecture The CLI is organized into command files and utility modules: ``` src/cli/ ├── index.ts # Entry point, Commander.js setup, version check ├── commands/ │ ├── init.ts # Project initialization │ ├── dev.ts # File watching and sync │ ├── deploy.ts # Production deployment │ ├── add.ts # Resource scaffolding │ ├── status.ts # Local vs remote comparison │ ├── pull.ts # Pull remote to local │ ├── login.ts # Browser-based OAuth │ ├── logout.ts # Clear credentials │ └── whoami.ts # Current user info └── utils/ ├── loader.ts # Load resources from directories ├── extractor.ts # Build sync payload ├── project.ts # Load/save struere.json ├── convex.ts # API calls to Convex ├── scaffold.ts # File templates for new resources └── credentials.ts # Auth token management ``` ### Startup On every invocation, the CLI performs a version check against npm with a 2-second timeout. If a newer version is available, it displays an update notice. ## Sync Mechanism The CLI syncs your local definitions to the Convex backend. The sync flow is: 1. **Load** — Read all files from `agents/`, `entity-types/`, `roles/`, `triggers/`, and `tools/` directories 2. **Extract** — Build a sync payload using `extractSyncPayload()` 3. **Sync** — Send the payload to the `syncOrganization` Convex mutation 4. **Watch** (dev mode) — Monitor files with chokidar and re-sync on any change The sync payload contains all agents, entity types, roles, and triggers. Resources are upserted by slug or name, meaning the CLI handles both creation and updates transparently. ## Organization-Centric Design Struere uses an organization-centric architecture. A single project defines all resources for one organization: - All agents in `agents/` - All entity types in `entity-types/` - All roles in `roles/` - All triggers in `triggers/` - Shared custom tools in `tools/` This means you manage your entire platform configuration from a single codebase, with the CLI handling synchronization to the backend. --- ## struere init > Initialize an organization-centric project The `init` command scaffolds a new Struere project with all the directories and configuration needed to define agents, entity types, roles, triggers, and tools. ## Usage ```bash npx struere init ``` ### Options | Flag | Description | |------|-------------| | `--org ` | Specify the organization slug directly instead of selecting interactively | | `-y, --yes` | Skip prompts and use defaults | ## What It Does 1. **Authenticates** — If you are not logged in, opens a browser for OAuth authentication 2. **Creates project structure** — Scaffolds the required directories for your resources 3. **Writes `struere.json`** — Creates the project configuration file with your organization details 4. **Generates types** — Creates `.struere/types.d.ts` type declarations for your project ## Directory Structure After running `init`, your project will contain: ``` my-project/ ├── struere.json ├── agents/ ├── entity-types/ ├── roles/ ├── triggers/ └── tools/ └── index.ts ``` | Directory | Purpose | |-----------|---------| | `agents/` | Agent definitions using `defineAgent()` | | `entity-types/` | Entity type schemas using `defineEntityType()` | | `roles/` | Role definitions with policies, scope rules, and field masks using `defineRole()` | | `triggers/` | Trigger automations using `defineTrigger()` | | `tools/` | Custom tool definitions using `defineTools()` | ## struere.json The configuration file links your project to a Struere organization: ```json { "version": "2.0", "organization": { "id": "org_abc123", "slug": "acme-corp", "name": "Acme Corp" } } ``` | Field | Description | |-------|-------------| | `version` | Schema version for the configuration file | | `organization.id` | Your Clerk organization ID | | `organization.slug` | URL-friendly organization slug | | `organization.name` | Human-readable organization name | ## Organization-Centric Architecture Struere projects are organized around a single organization. All resources you define — agents, entity types, roles, triggers, and tools — belong to that organization. This means: - One project per organization - All agents share the same entity types and roles - Custom tools defined in `tools/index.ts` are available to any agent in the organization - The CLI syncs the entire project state on every change ## Auto-Init You do not need to run `init` explicitly. If you run `struere dev` or other commands without a `struere.json` file present, the CLI will automatically run the initialization flow first. ## Next Steps After initializing, create your first resources: ```bash npx struere add agent my-agent npx struere add entity-type customer npx struere add role support npx struere dev ``` --- ## struere dev > Watch files and sync to Convex on change The `dev` command is your primary development workflow. It loads all resource definitions from your project, syncs them to the Convex backend in the **development** environment, and watches for file changes to re-sync automatically. ## Usage ```bash npx struere dev ``` ### Options | Flag | Description | |------|-------------| | `--force` | Skip the destructive sync confirmation prompt. By default, `dev` warns you if the sync would delete remote resources not present locally. | ## How It Works The dev command follows this flow: 1. **Auto-init** — If no `struere.json` exists, runs the initialization flow 2. **Auto-login** — If not authenticated, opens a browser for OAuth 3. **Load resources** — Reads all files from `agents/`, `entity-types/`, `roles/`, `triggers/`, and `tools/` directories 4. **Build sync payload** — Assembles all resources into a single payload via `extractSyncPayload()` 5. **Sync to Convex** — Sends the payload to the `syncOrganization` mutation 6. **Watch for changes** — Uses chokidar to monitor all resource directories 7. **Re-sync on change** — When any file is added, modified, or deleted, reloads and re-syncs ## Sync Payload The CLI assembles all your local definitions into a single sync payload: ```typescript { agents: [...], entityTypes: [...], roles: [...], triggers: [...] } ``` Each resource is upserted by its slug or name, so both new resources and updates are handled transparently. ## Sync HTTP Request Under the hood, the CLI sends the payload to Convex via an HTTP mutation: ``` POST /api/mutation Authorization: Bearer {token} Content-Type: application/json { "path": "sync:syncOrganization", "args": { "agents": [...], "entityTypes": [...], "roles": [...], "triggers": [...] } } ``` ## Environment The `dev` command always syncs to the **development** environment. All entity types, roles, agent configurations, and triggers are scoped to development. This ensures your changes do not affect production data or behavior. To deploy to production, use `struere deploy`. ## File Watching The CLI watches the following directories for changes: - `agents/` — Agent definition files - `entity-types/` — Entity type schema files - `roles/` — Role definition files - `triggers/` — Trigger definition files - `tools/` — Custom tool definition files - `evals/` — Eval suite definition files When a file is added, modified, or deleted, the CLI: 1. Reloads all resources from disk 2. Builds a fresh sync payload 3. Sends the full payload to Convex This means the sync is always a complete snapshot of your local state, not incremental changes. ## Example Workflow ```bash npx struere dev ``` With the dev command running, open a separate terminal and create resources: ```bash npx struere add agent scheduler ``` The dev process will detect the new file and sync it automatically. Edit the generated `agents/scheduler.ts` file and save — the changes will sync within seconds. ## Convex Sync Functions The backend processes the sync payload through these functions: | Function | Purpose | |----------|---------| | `syncOrganization` | Bulk upsert all resources (agents, entity types, roles, triggers) | | `getSyncState` | Return current remote state for comparison | The sync helpers upsert resources by slug, creating new records or updating existing ones as needed. ## Troubleshooting If the sync fails, the CLI will display the error message from Convex. Common issues include: - **Invalid schema** — Entity type schemas must be valid JSON Schema objects - **Duplicate slugs** — Each resource type must have unique slugs within an organization - **Authentication expired** — Re-run `struere login` to refresh your token --- ## struere add > Scaffold new agents, entity types, roles, and triggers The `add` command scaffolds a new resource file with a starter template in the appropriate directory. ## Usage ```bash npx struere add ``` ## Resource Types | Type | Directory | Definition Function | |------|-----------|-------------------| | `agent` | `agents/` | `defineAgent()` | | `entity-type` | `entity-types/` | `defineEntityType()` | | `role` | `roles/` | `defineRole()` | | `trigger` | `triggers/` | `defineTrigger()` | | `eval` / `suite` | `evals/` | `defineEvalSuite()` | ## Examples ### Scaffold an Agent ```bash npx struere add agent scheduler ``` Creates `agents/scheduler.ts`: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Scheduler", slug: "scheduler", version: "0.1.0", systemPrompt: "You are a scheduling assistant...", model: { provider: "anthropic", name: "claude-sonnet-4", }, tools: ["entity.query", "event.emit"], }) ``` ### Scaffold an Entity Type ```bash npx struere add entity-type customer ``` Creates `entity-types/customer.ts`: ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ name: "Customer", slug: "customer", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string", format: "email" }, }, required: ["name", "email"], }, searchFields: ["name", "email"], }) ``` ### Scaffold a Role ```bash npx struere add role support ``` Creates `roles/support.ts`: ```typescript import { defineRole } from 'struere' export default defineRole({ name: "support", description: "Support team role", policies: [ { resource: "customer", actions: ["list", "read"], effect: "allow" }, ], }) ``` ### Scaffold a Trigger ```bash npx struere add trigger notify-on-signup ``` Creates `triggers/notify-on-signup.ts`: ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Notify on Signup", slug: "notify-on-signup", on: { entityType: "customer", action: "created", }, actions: [ { tool: "event.emit", args: { eventType: "customer.signup", entityId: "{{trigger.entityId}}", }, }, ], }) ``` ## With Dev Running If you have `struere dev` running in another terminal, newly scaffolded files will be detected and synced to Convex automatically. You can immediately edit the generated file and see your changes reflected in the development environment. ## Naming Conventions The `` argument is used to generate both the filename and the resource slug: - Filename: `.ts` (e.g., `scheduler.ts`, `notify-on-signup.ts`) - Slug: derived from the name (e.g., `scheduler`, `notify-on-signup`) - Display name: derived from the name with capitalization (e.g., `Scheduler`, `Notify on Signup`) --- ## struere status > Compare local vs remote state The `status` command compares your local file definitions against the current remote state in Convex, showing what would change on the next sync. ## Usage ```bash npx struere status ``` ## What It Does The `status` command: 1. Loads all resource definitions from your local directories (`agents/`, `entity-types/`, `roles/`, `triggers/`) 2. Fetches the current remote state via the `getSyncState` Convex function 3. Compares each resource and displays the differences ## Output The status output categorizes resources into: - **New** — Resources that exist locally but not remotely (will be created on sync) - **Modified** — Resources that exist in both places but have differences (will be updated on sync) - **Deleted** — Resources that exist remotely but not locally (will be removed on sync) - **Unchanged** — Resources that are identical locally and remotely ## Example Output ``` Agents: + scheduler (new) ~ support-agent (modified) = onboarding-agent (unchanged) Entity Types: + invoice (new) = customer (unchanged) = teacher (unchanged) Roles: = admin (unchanged) ~ teacher (modified) - legacy-role (deleted) Triggers: + notify-on-signup (new) ``` ## When to Use Status - **Before deploying** — Review what will change before running `struere deploy` - **After pulling** — Verify that your local state matches remote after a `pull` - **Debugging sync issues** — Identify resources that are out of sync - **Code review** — See at a glance what a set of file changes will do to the remote state ## Workflow Integration A safe deployment workflow using `status`: ```bash npx struere status npx struere dev npx struere status npx struere deploy ``` 1. Check what will change 2. Sync to development and test 3. Verify the state is clean 4. Deploy to production ## Relationship with Other Commands | Command | What it does | |---------|-------------| | `struere status` | Read-only comparison, changes nothing | | `struere dev` | Syncs local to remote (development environment) | | `struere pull` | Syncs remote to local (overwrites local files) | | `struere deploy` | Promotes development to production | --- ## struere pull > Pull remote resources to local files The `pull` command downloads the current remote state from Convex and writes it to local files, creating or updating resource definitions in your project directories. ## Usage ```bash npx struere pull ``` ## What It Does The `pull` command: 1. Fetches the current state of all resources from your Convex backend 2. Generates local definition files for each resource 3. Writes the files to the appropriate directories (`agents/`, `entity-types/`, `roles/`, `triggers/`) ## When to Use Pull The `pull` command is useful when: - **Resources were created via the dashboard** — If you or a team member created agents, entity types, roles, or triggers through the web dashboard, `pull` brings those definitions into your local project so they can be managed as code. - **Onboarding to an existing project** — When joining a team that already has resources configured in Convex, `pull` gives you the local files to start working with. - **Recovering local files** — If local files were accidentally deleted or corrupted, `pull` restores them from the remote state. - **Switching machines** — When setting up a new development environment, `pull` populates your project with the current remote definitions. ## Example Workflow A common scenario is creating resources in the dashboard and then pulling them locally for version control: ```bash npx struere pull git add agents/ entity-types/ roles/ triggers/ git commit -m "Pull remote resources to local definitions" ``` After pulling, you can modify the local files and use `struere dev` to sync changes back. ## Relationship with Dev and Status | Command | Direction | Purpose | |---------|-----------|---------| | `struere pull` | Remote to local | Download remote state as local files | | `struere dev` | Local to remote | Sync local files to remote | | `struere status` | Comparison | Show differences without changing anything | A typical workflow might be: 1. `struere pull` — Get the latest remote state 2. Edit local files — Make your changes 3. `struere status` — Review what will change 4. `struere dev` — Sync changes to the development environment --- ## struere deploy > Deploy all agents to production The `deploy` command promotes all agent configurations to the **production** environment. ## Usage ```bash npx struere deploy ``` ## What It Does The deploy command calls the `deployAllAgents` function on the Convex backend. This takes all agent configurations currently in the development environment and copies them to production, making them accessible via production API keys. ## Development vs Production Struere enforces full environment isolation between development and production: | Aspect | `struere dev` | `struere deploy` | |--------|---------------|------------------| | Target environment | `development` | `production` | | Data isolation | Development entities, threads, events | Production entities, threads, events | | API keys | Development keys only | Production keys only | | Agent configs | Stored as development configs | Promoted to production configs | | Role resolution | Development roles | Production roles | When you run `struere dev`, all synced resources are created in the development environment. Your development API keys interact with development data, and your agents use development configurations. When you run `struere deploy`, agent configurations are promoted to production. Production API keys will now use the deployed configurations. Production data remains completely separate from development data. ## Deployment Flow ``` Local files ──[struere dev]──► Development environment │ [struere deploy] │ ▼ Production environment ``` ## Environment Scoping The following resources are scoped per environment: - Agent configurations (model, system prompt, tools) - Entity types and entities - Roles, policies, scope rules, and field masks - Threads and messages - Events and executions - Trigger runs - API keys - Installed packs Agents themselves (name, slug, description) are shared across environments — only their configurations are environment-specific. ## API Key Environments API keys carry an `environment` field. When an API request arrives, the environment is extracted from the API key and threaded through the entire request: - Config lookup uses the key's environment - Threads are created in that environment - Actor context carries the environment - Tool execution respects environment boundaries - Events are logged with the environment A development API key cannot access production data, and vice versa. ## Production Checklist Before deploying, verify: 1. **Test in development** — Use `struere dev` and development API keys to test your agents thoroughly 2. **Review with `struere status`** — Compare your local definitions against the remote state 3. **Check permissions** — Ensure roles and policies are correctly configured for production use 4. **Verify API keys** — Create production API keys in the dashboard for your applications --- # API Reference --- ## API Overview > HTTP endpoints for interacting with Struere Struere exposes HTTP endpoints through its Convex backend for agent communication, webhook processing, and health monitoring. Your base URL is your Convex deployment URL (the `CONVEX_SITE_URL`). ## Authentication Chat endpoints authenticate via **Bearer token** using API keys. API keys are created in the Struere dashboard under **API Keys** and are scoped to a specific environment (`development` or `production`). ``` Authorization: Bearer sk_dev_abc123... ``` The environment of the API key determines which environment the request operates in. A development API key accesses development agents, entities, and configurations. A production API key accesses production data. There is no way to cross environments with a single key. API keys are validated by computing a SHA-256 hash and looking up the hashed value in the database. ## Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/health` | None | Health check | | `POST` | `/v1/chat` | Bearer token | Chat with an agent by agent ID | | `POST` | `/v1/agents/:slug/chat` | Bearer token | Chat with an agent by slug | | `POST` | `/webhook/clerk` | None | Clerk user/organization sync webhook | | `POST` | `/webhook/kapso/project` | HMAC signature | WhatsApp phone number connection events | | `POST` | `/webhook/kapso/messages` | HMAC signature | WhatsApp inbound messages and status updates | | `POST` | `/webhook/flow` | None | Flow payment status updates | | `POST` | `/webhook/polar` | HMAC signature | Polar payment/billing events | ## GET /health Returns the current server status and timestamp. **Request:** ```bash curl https://your-deployment.convex.site/health ``` **Response:** ```json { "status": "ok", "timestamp": 1710500000000 } ``` ## POST /v1/chat Send a message to an agent identified by its Convex document ID. See the [Chat API](./chat) documentation for full details. ## POST /v1/agents/:slug/chat Send a message to an agent identified by its slug. This is the preferred endpoint for external integrations as slugs are human-readable and stable across deployments. See the [Chat API](./chat) documentation for full details. ## Webhook Endpoints Webhook endpoints receive events from external services. See the [Webhooks](./webhooks) documentation for details on each webhook. ## Error Responses All endpoints return JSON error responses with appropriate HTTP status codes: **401 Unauthorized** — Missing or invalid API key: ```json { "error": "Unauthorized" } ``` **400 Bad Request** — Missing required fields: ```json { "error": "agentId and message are required" } ``` **500 Internal Server Error** — Server-side execution failure: ```json { "error": "Error description" } ``` ## Rate Limiting Rate limits are enforced at the Convex platform level. Refer to your Convex plan for specific limits on function calls and bandwidth. ## CORS The HTTP endpoints do not set CORS headers by default. For browser-based integrations, use the Convex React client which connects over WebSocket rather than HTTP. --- ## Chat API > Send messages to agents via HTTP The Chat API allows you to send messages to agents and receive responses over HTTP. There are two endpoints: one that identifies agents by their Convex document ID, and one that uses human-readable slugs. ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/v1/chat` | Chat by agent ID | | `POST` | `/v1/agents/:slug/chat` | Chat by agent slug | Both endpoints require a Bearer token (API key) for authentication. The API key determines which **environment** (development or production) the request operates in. ## POST /v1/chat Send a message to an agent by its Convex document ID. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "agentId": "abc123def456", "message": "Hello, can you help me schedule a session?", "threadId": "thread_xyz789", "externalThreadId": "my-app:user-123" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `agentId` | `string` | Yes | The Convex document ID of the agent | | `message` | `string` | Yes | The user's message to the agent | | `threadId` | `string` | No | An existing thread ID to continue a conversation | | `externalThreadId` | `string` | No | An external identifier for thread reuse (e.g., `"whatsapp:+1234567890"`) | ### Response ```json { "threadId": "thread_xyz789", "message": "I'd be happy to help you schedule a session. What subject and time works best?", "usage": { "inputTokens": 1250, "outputTokens": 45, "totalTokens": 1295 } } ``` | Field | Type | Description | |-------|------|-------------| | `threadId` | `string` | The thread ID for this conversation | | `message` | `string` | The agent's response text | | `usage` | `object` | Token usage for this interaction | | `usage.inputTokens` | `number` | Number of input tokens consumed | | `usage.outputTokens` | `number` | Number of output tokens generated | | `usage.totalTokens` | `number` | Total tokens used | ### Example ```bash curl -X POST https://your-deployment.convex.site/v1/chat \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "agentId": "jd72k3m4n5p6q7r8", "message": "What sessions are scheduled for tomorrow?" }' ``` ## POST /v1/agents/:slug/chat Send a message to an agent by its slug. This is the preferred endpoint for integrations as slugs are stable and human-readable. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "message": "Hello, can you help me schedule a session?", "threadId": "thread_xyz789", "externalThreadId": "my-app:user-123" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `message` | `string` | Yes | The user's message to the agent | | `threadId` | `string` | No | An existing thread ID to continue a conversation | | `externalThreadId` | `string` | No | An external identifier for thread reuse | Note that `agentId` is **not** needed since the agent is identified by the `:slug` URL parameter. ### Response The response format is identical to the `/v1/chat` endpoint. ### Example ```bash curl -X POST https://your-deployment.convex.site/v1/agents/scheduler/chat \ -H "Authorization: Bearer sk_prod_xyz789" \ -H "Content-Type: application/json" \ -d '{ "message": "Book a math session with Mr. Smith for Tuesday at 3 PM" }' ``` ## Thread Management ### Creating New Threads If neither `threadId` nor `externalThreadId` is provided, a new thread is created automatically. The response includes the `threadId` which you should store for subsequent messages. ### Continuing Conversations Pass the `threadId` from a previous response to continue the conversation. The agent receives the full message history from the thread, maintaining context across messages. ```bash curl -X POST https://your-deployment.convex.site/v1/agents/support/chat \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "message": "Actually, make that Thursday instead", "threadId": "jd72k3m4n5p6q7r8" }' ``` ### External Thread IDs The `externalThreadId` field allows you to map external identifiers to Struere threads. If a thread with the given `externalThreadId` already exists, it is reused. Otherwise, a new thread is created. This is useful for integrations where you want to maintain a single conversation thread per external user or channel: ```json { "message": "What is my account balance?", "externalThreadId": "slack:U12345678" } ``` Common patterns: - `whatsapp:+1234567890` for WhatsApp conversations - `slack:U12345678` for Slack user threads - `app:user-abc123` for your application's user IDs ## Environment Scoping The API key determines the environment for the entire request: - **Development API keys** (`sk_dev_...`) access development agent configurations, development entities, and development threads - **Production API keys** (`sk_prod_...`) access production agent configurations, production entities, and production threads There is no way to specify the environment in the request body. It is always derived from the API key. ## Execution Flow When a chat request arrives: 1. The API key is validated and the environment is extracted 2. The agent and its configuration are loaded for the matching environment 3. A thread is retrieved or created 4. The system prompt is processed (template variables and function calls resolved) 5. The LLM is called in a loop (up to 10 iterations) to handle tool calls 6. Each tool call is permission-checked against the actor context 7. The final response, thread ID, and usage stats are returned ## Error Responses **401 Unauthorized:** ```json { "error": "Unauthorized" } ``` **400 Bad Request:** ```json { "error": "agentId and message are required" } ``` ```json { "error": "message is required" } ``` **500 Internal Server Error:** ```json { "error": "Agent not found" } ``` ```json { "error": "No active config found for agent \"scheduler\" in production" } ``` --- ## Webhooks > Inbound webhook endpoints for external integrations Struere provides webhook endpoints for receiving events from external services. These endpoints are registered in the Convex HTTP router and process incoming events to keep your platform synchronized with third-party systems. ## Clerk Webhook **Endpoint:** `POST /webhook/clerk` Receives user, organization, and membership events from Clerk to keep the Struere database in sync with your authentication provider. ### Supported Event Types | Event Type | Action | |------------|--------| | `user.created` | Creates or updates the user record in Struere | | `user.updated` | Updates the user's email and name | | `organization.created` | Creates the organization in Struere | | `organization.updated` | Updates the organization's name and slug | | `organization.deleted` | Marks the organization as deleted | | `organizationMembership.created` | Links a user to an organization with their role | | `organizationMembership.updated` | Updates the user's role within the organization | | `organizationMembership.deleted` | Removes a user's membership from the organization | ### Role Mapping Clerk roles are mapped to Struere roles: | Clerk Role | Struere Role | |------------|--------------| | `org:admin` | `admin` | | `org:owner` | `admin` | | All other roles | `member` | ### Setup Configure the Clerk webhook in your Clerk Dashboard: 1. Navigate to **Webhooks** in the Clerk Dashboard 2. Create a new endpoint pointing to `https://your-deployment.convex.site/webhook/clerk` 3. Select the event types listed above 4. Save the endpoint ### Response Returns `200` with `{"received": true}` on success, or `500` if processing fails. ## WhatsApp Webhooks (Kapso) WhatsApp integration uses the Kapso service as an intermediary. Two webhook endpoints handle different aspects of the WhatsApp connection. ### Project Webhook **Endpoint:** `POST /webhook/kapso/project` Receives phone number connection events when a WhatsApp number is linked through the Kapso setup flow. **Authentication:** HMAC-SHA256 signature verification via the `X-Kapso-Signature` header. The signature is computed over the raw request body using the `KAPSO_WEBHOOK_SECRET` environment variable. **Event Types:** | Event Type | Action | |------------|--------| | `whatsapp.phone_number.created` | Updates the WhatsApp connection status to `connected` and registers the message webhook | When a phone number is connected, the system: 1. Finds the matching WhatsApp connection by Kapso customer ID 2. Stores the Kapso phone number ID and phone number 3. Sets the connection status to `connected` 4. Registers the messages webhook URL with Kapso for that phone number ### Messages Webhook **Endpoint:** `POST /webhook/kapso/messages` Receives inbound WhatsApp messages and message status updates. **Authentication:** HMAC-SHA256 signature verification via the `X-Kapso-Signature` header. **Event Types:** | Event Type | Action | |------------|--------| | `whatsapp.message.received` | Processes and stores the inbound message, routes to assigned agent | | `whatsapp.message.status_update` | Updates the delivery status of an outbound message | #### Inbound Message Flow When an inbound message arrives: 1. The phone number ID is used to look up the WhatsApp connection and determine the organization 2. The message is stored in the `whatsappMessages` table with deduplication by `messageId` 3. If the message is new and contains text, `scheduleAgentRouting` is called 4. The agent routing mutation finds the connected agent and schedules `routeInboundToAgent` 5. The routing action creates or reuses a thread with `externalId` set to `whatsapp:{phoneNumber}` 6. The agent processes the message via `chatAuthenticated` using a system actor context 7. The agent's response is sent back via the Kapso API #### Status Updates Status updates (`sent`, `delivered`, `read`, `failed`) are applied to the matching outbound message record. ### Required Environment Variables | Variable | Description | |----------|-------------| | `KAPSO_WEBHOOK_SECRET` | Shared secret for HMAC signature verification | ## Flow Payment Webhook **Endpoint:** `POST /webhook/flow` Receives payment status updates from the Flow payment provider via form-encoded POST data. ### Request Format The webhook receives a `token` parameter via `application/x-www-form-urlencoded` form data. This token is used to verify the payment status with the Flow API. ### Processing Flow 1. Extract the `token` from the form data 2. Query all active Flow integration configurations 3. For each configuration, call Flow's payment verification API with the token 4. Based on the returned status: | Flow Status | Action | |-------------|--------| | `2` (Paid) | Mark the payment entity as paid with the current timestamp | | `3` (Rejected) | Mark the payment entity as failed with the status message | | `4` (Cancelled) | Mark the payment entity as failed with the status message | ### Response Always returns `200 OK` to acknowledge receipt of the webhook. ## Polar Webhook **Endpoint:** `POST /webhook/polar` Receives payment events from the Polar billing platform. **Authentication:** Standard Webhook Signature verification using `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. The signature is verified against the `POLAR_WEBHOOK_SECRET` environment variable. Timestamps older than 5 minutes are rejected. **Event Types:** | Event Type | Action | |------------|--------| | `order.paid` | Adds credits to the organization's account based on the order subtotal amount | ### Required Environment Variables | Variable | Description | |----------|-------------| | `POLAR_WEBHOOK_SECRET` | Secret key for webhook signature verification | ## Webhook Security All webhooks that handle sensitive operations use signature verification: | Webhook | Verification Method | |---------|-------------------| | Clerk | Configured in Clerk Dashboard (Svix signatures) | | Kapso (WhatsApp) | HMAC-SHA256 via `X-Kapso-Signature` header | | Flow | Token-based verification via Flow API callback | | Polar | Standard Webhook Signature (HMAC-SHA256 with base64) | --- # Integrations --- ## WhatsApp Integration > WhatsApp messaging integration via Kapso Struere integrates with WhatsApp through the **Kapso** service, which manages the WhatsApp Business API connection. This allows agents to receive and respond to WhatsApp messages, with conversations persisted and routed through the platform's standard agent execution pipeline. ## Architecture ``` WhatsApp User | v WhatsApp Business API | v Kapso Service (manages phone numbers, message routing) | v Convex Webhooks (/webhook/kapso/project, /webhook/kapso/messages) | v Struere Backend (message storage, agent routing) | v Agent LLM Execution | v Kapso API (outbound message delivery) | v WhatsApp User receives response ``` ## Database Tables ### whatsappConnections Stores the connection state between an organization and a WhatsApp phone number. Scoped by environment. | Field | Type | Description | |-------|------|-------------| | `organizationId` | `Id<"organizations">` | The owning organization | | `environment` | `"development" \| "production"` | Environment scope | | `status` | `"disconnected" \| "pending_setup" \| "connected"` | Current connection state | | `phoneNumber` | `string?` | The connected phone number | | `kapsoCustomerId` | `string?` | Kapso customer identifier | | `kapsoPhoneNumberId` | `string?` | Kapso phone number identifier | | `agentId` | `Id<"agents">?` | The agent assigned to handle inbound messages | | `setupLinkUrl` | `string?` | URL for the phone number setup flow | | `lastConnectedAt` | `number?` | Timestamp of last successful connection | | `lastDisconnectedAt` | `number?` | Timestamp of last disconnection | ### whatsappMessages Stores all inbound and outbound messages with delivery status tracking. | Field | Type | Description | |-------|------|-------------| | `organizationId` | `Id<"organizations">` | The owning organization | | `direction` | `"inbound" \| "outbound"` | Message direction | | `phoneNumber` | `string` | The external phone number | | `messageId` | `string` | Unique message identifier from Kapso/WhatsApp | | `type` | `string` | Message type (e.g., `"text"`) | | `text` | `string?` | Message text content | | `threadId` | `Id<"threads">?` | Linked conversation thread | | `status` | `string` | Delivery status (`"received"`, `"sent"`, `"delivered"`, `"read"`, `"failed"`) | | `createdAt` | `number` | Message timestamp | ## Setup Flow ### 1. Enable the Integration Enable WhatsApp for your organization and environment through the dashboard or API: ```typescript await whatsapp.enableWhatsApp({ environment: "development" }) ``` This creates an integration config entry with provider `"whatsapp"` and status `"active"`. ### 2. Start Phone Setup Initiate the WhatsApp phone number connection: ```typescript await whatsapp.setupWhatsApp({ environment: "development" }) ``` This triggers an asynchronous flow: 1. A Kapso customer is created for your organization 2. A setup link URL is generated 3. The connection status moves to `"pending_setup"` 4. The setup link is stored on the connection record ### 3. Complete Phone Connection The user follows the setup link to connect their WhatsApp Business phone number through Kapso's interface. Once complete, the `whatsapp.phone_number.created` webhook fires and: 1. The connection status updates to `"connected"` 2. The Kapso phone number ID and phone number are stored 3. A message webhook is registered with Kapso pointing to `/webhook/kapso/messages` ### 4. Assign an Agent Assign an agent to handle inbound messages: ```typescript await whatsapp.setWhatsAppAgent({ agentId: "agent_id_here", environment: "development", }) ``` ## Inbound Message Routing When a WhatsApp message arrives, the following sequence executes: ``` POST /webhook/kapso/messages | v Verify HMAC-SHA256 signature | v Look up connection by kapsoPhoneNumberId | v processInboundMessage (store message, deduplicate by messageId) | v scheduleAgentRouting (mutation, schedules action via ctx.scheduler) | v routeInboundToAgent (action) | v threads.getOrCreate with externalId = "whatsapp:{phoneNumber}" | v agent.chatAuthenticated (system actor, no user) | v Send response via Kapso sendTextMessage API | v storeOutboundMessage (persist response in whatsappMessages) ``` ### Thread Reuse Conversations with a given phone number are grouped into a single thread using the `externalId` pattern `whatsapp:{phoneNumber}`. The `threads.getOrCreate` mutation looks up existing threads by this external ID, ensuring all messages from the same WhatsApp number flow through the same conversation context. ### System Actor Context Inbound WhatsApp messages are processed using a **system actor context** because there is no authenticated Clerk user for the incoming message. The system actor has `isOrgAdmin: true` and operates with full permissions within the organization's environment. ## Outbound Messages Agents send responses back to WhatsApp users through the Kapso API. When the agent's LLM loop completes: 1. The response text is extracted from the agent's reply 2. The `sendTextMessage` function calls the Kapso API with the phone number and text 3. The outbound message is stored in `whatsappMessages` with its Kapso message ID 4. Delivery status updates arrive via the status update webhook ## Message Status Tracking Outbound message status progresses through these states: ``` sent -> delivered -> read \ -> failed ``` Status updates are received via the `whatsapp.message.status_update` event type on the messages webhook and applied to the corresponding message record. ## WhatsApp Tools Agents can also interact with WhatsApp programmatically through built-in WhatsApp tools: ### whatsapp.send Send a text message to a phone number: ```typescript { to: "+1234567890", text: "Your session is confirmed for tomorrow at 3 PM." } ``` ### whatsapp.getConversation Retrieve message history for a phone number: ```typescript { phoneNumber: "+1234567890", limit: 20 } ``` ### whatsapp.getStatus Check the current WhatsApp connection status for the organization. ## Template Messages For sending structured WhatsApp templates (pre-approved message formats), use the `sendTemplate` action: ```typescript await whatsapp.sendTemplate({ threadId: "thread_id", templateName: "session_reminder", language: "en", components: [ { type: "body", parameters: [ { type: "text", text: "Tuesday, March 15" }, { type: "text", text: "3:00 PM" }, ], }, ], }) ``` Template messages are stored with the text `[Template: templateName]` in the message history. ## Disconnecting To disconnect WhatsApp from an environment: ```typescript await whatsapp.disconnectWhatsApp({ environment: "development" }) ``` This sets the connection status to `"disconnected"` and clears the phone number and setup link fields. The Kapso customer record is retained for potential reconnection. ## Required Environment Variables | Variable | Location | Description | |----------|----------|-------------| | `KAPSO_API_KEY` | Convex | API key for the Kapso service | | `KAPSO_WEBHOOK_SECRET` | Convex | Shared secret for webhook signature verification | | `CONVEX_SITE_URL` | Convex | Your Convex site URL (used to construct webhook callback URLs) | --- ## Flow Payments > Payment processing with Flow integration Struere integrates with **Flow** (flow.cl) as a payment provider for processing payments within the platform. This integration supports payment link generation, HMAC-SHA256 request signing, webhook-based status updates, and reconciliation for missed webhooks. ## Configuration Flow integration is configured per organization and environment through the `integrationConfigs` table with provider `"flow"`. The configuration requires: | Field | Type | Description | |-------|------|-------------| | `apiUrl` | `string` | Flow API base URL | | `apiKey` | `string` | Your Flow API key | | `secretKey` | `string` | Your Flow secret key for request signing | | `webhookBaseUrl` | `string` | Base URL for webhook callbacks | Configure the integration through the Struere dashboard at **Settings > Integrations > Flow**. ## Payment Link Generation To create a payment link, the platform constructs a signed request to the Flow API: ```typescript interface CreatePaymentLinkParams { organizationId: Id<"organizations"> environment: "development" | "production" paymentId: Id<"entities"> amount: number currency: string description: string customerEmail: string returnUrl: string } ``` The payment creation flow: 1. Load the Flow configuration for the organization and environment 2. Build the order data with the API key, amount, currency, email, and callback URLs 3. Sign the request using HMAC-SHA256 with the secret key 4. POST to Flow's `/payment/create` endpoint 5. Store the returned payment link URL and Flow order ID on the payment entity 6. Return the payment link URL for the customer ### Request Signing All requests to the Flow API are signed using HMAC-SHA256: 1. Sort the request parameters alphabetically by key 2. Concatenate them as `key1=value1&key2=value2&...` 3. Compute the HMAC-SHA256 digest using the secret key 4. Append the hex-encoded signature as the `s` parameter ## Payment Status Webhook **Endpoint:** `POST /webhook/flow` Flow sends payment status updates as form-encoded POST data with a `token` parameter. ### Processing Flow ``` Flow sends POST /webhook/flow with token | v Extract token from form data | v Query all active Flow configurations | v For each config, verify payment status via Flow API | v Map status code to action: Status 2 (Paid) -> markAsPaid Status 3 (Rejected) -> markAsFailed Status 4 (Cancelled) -> markAsFailed ``` ### Status Codes | Flow Status | Meaning | Struere Action | |-------------|---------|----------------| | `1` | Pending | No action (payment still in progress) | | `2` | Paid | Mark payment as paid with timestamp | | `3` | Rejected | Mark payment as failed with reason | | `4` | Cancelled | Mark payment as failed with reason | ### Verification The token received in the webhook is verified against the Flow API by calling the `payment/getStatus` endpoint with a signed request. This confirms the payment status directly with Flow rather than trusting the webhook payload alone. ## Session Lifecycle Integration Payments are tied to the session lifecycle in the tutoring domain: ``` pending_payment ──[payment.success]──> scheduled | ┌─────────────────────┼─────────────────────┐ | | | v v v cancelled in_progress no_show | v completed ``` ### Status Transitions 1. **Session created** — Status is `pending_payment`. A payment link is generated and sent to the guardian. 2. **Payment completed** — The webhook fires, the payment entity is marked as paid, and the session transitions to `scheduled`. 3. **Session occurs** — Status moves to `in_progress` when the session starts. 4. **Session ends** — Status moves to `completed`. Credits are consumed from the guardian's entitlement. ### Credit Consumption When a session completes: 1. The system looks up the guardian's active entitlement for the student 2. The remaining credits are decremented 3. An event is emitted recording the credit consumption ## Reconciliation For missed webhooks or uncertain payment states, the platform supports manual reconciliation by checking payment status directly with the Flow API: ```typescript interface FlowPaymentStatus { flowOrder: number status: string statusMessage: string amount: number currency: string payer: string } ``` The `checkFlowOrderStatus` function queries Flow's `/payment/getStatusByFlowOrder` endpoint using the Flow order ID, allowing the system to verify and update payment status independently of webhooks. ## Payment Entity Schema Payments are stored as entities of type `payment` with the following data fields: | Field | Type | Description | |-------|------|-------------| | `guardianId` | `string` | The guardian entity ID responsible for payment | | `amount` | `number` | Payment amount | | `currency` | `string` | Currency code (e.g., `"CLP"`) | | `status` | `string` | Payment status (`"pending"`, `"paid"`, `"failed"`) | | `providerReference` | `string` | The Flow order ID | | `paymentLinkUrl` | `string` | The generated payment link URL | | `sessionId` | `string` | The associated session entity ID | | `paidAt` | `number?` | Timestamp when payment was confirmed | | `failedReason` | `string?` | Reason for payment failure | ## Required Environment Variables Flow credentials are stored in the `integrationConfigs` table rather than as environment variables. The configuration is loaded at runtime from the database for each organization and environment. No Convex environment variables are required specifically for Flow. All credentials are managed through the integration configuration in the dashboard. --- # Reference --- ## Model Configuration > Available AI model providers and pricing Struere supports multiple LLM providers for agent execution. Each agent can be configured with a specific provider, model, and inference parameters. ## Available Providers | Provider | Model Names | Notes | |----------|-------------|-------| | `anthropic` | `claude-haiku-4-5`, `claude-sonnet-4`, `claude-opus-4-5` | Default provider | | `openai` | `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo` | Requires `OPENAI_API_KEY` | | `google` | `gemini-1.5-pro`, `gemini-1.5-flash` | Requires `GOOGLE_API_KEY` | | `custom` | Any model name | Requires `apiKey` in model config. Use for self-hosted or alternative providers. | ## Anthropic Models Anthropic models are the default provider and require the `ANTHROPIC_API_KEY` environment variable on your Convex deployment. ### Pricing | Model | Input (per MTok) | Output (per MTok) | Best For | |-------|-------------------|--------------------|----------| | `claude-haiku-4-5` | $1 | $5 | Best cost-to-intelligence ratio for high-volume tasks | | `claude-sonnet-4` | $3 | $15 | **Default** — Strong reasoning with balanced cost | | `claude-opus-4-5` | $5 | $25 | Most capable, research-grade tasks requiring deep analysis | ### Choosing a Model - **claude-haiku-4-5** — Use for high-volume, cost-sensitive agents. Fast and capable enough for entity management, scheduling, and standard workflows. - **claude-sonnet-4** — The default model. Strong reasoning with balanced cost, suitable for most agent tasks including multi-step planning and nuanced decision-making. - **claude-opus-4-5** — Use sparingly for agents that require the highest possible capability, such as complex analysis or research tasks. ## Configuration Options The `model` field in an agent definition accepts the following options: ```typescript model: { provider: "anthropic", name: "claude-sonnet-4", temperature?: 0.7, maxTokens?: 4096, apiKey?: "sk-...", } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `provider` | `string` | `"anthropic"` | The LLM provider (`"anthropic"`, `"openai"`, `"google"`, or `"custom"`) | | `name` | `string` | `"claude-sonnet-4"` (full ID: `claude-sonnet-4-20250514`) | The model name | | `temperature` | `number` | `0.7` | Controls randomness. Lower values (0.0-0.3) produce more deterministic output. Higher values (0.7-1.0) produce more creative output. | | `maxTokens` | `number` | `4096` | Maximum number of tokens in the model's response | | `apiKey` | `string` | — | API key override. Required for `custom` provider. For standard providers, the key is read from environment variables. | ## Default Configuration If no model is specified in the agent definition, the default configuration is used: ```typescript { provider: "anthropic", name: "claude-sonnet-4", temperature: 0.7, maxTokens: 4096, } ``` ## Examples ### Cost-Optimized Agent For high-volume, straightforward tasks: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Data Entry Agent", slug: "data-entry", version: "0.1.0", systemPrompt: "You process incoming data and create entities.", model: { provider: "anthropic", name: "claude-haiku-4-5", temperature: 0.1, maxTokens: 2048, }, tools: ["entity.create", "entity.query"], }) ``` ### High-Capability Agent For complex reasoning and multi-step workflows: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Strategy Advisor", slug: "strategy", version: "0.1.0", systemPrompt: "You analyze business data and provide strategic recommendations.", model: { provider: "anthropic", name: "claude-sonnet-4", temperature: 0.5, maxTokens: 8192, }, tools: ["entity.query", "event.query"], }) ``` ### Deterministic Agent For tasks requiring consistent, reproducible output: ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Report Generator", slug: "reports", version: "0.1.0", systemPrompt: "You generate structured reports from entity data.", model: { provider: "anthropic", name: "claude-haiku-4-5", temperature: 0.0, maxTokens: 4096, }, tools: ["entity.query", "event.query"], }) ``` ### OpenAI Provider ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "GPT Agent", slug: "gpt-agent", version: "0.1.0", systemPrompt: "You assist with general queries.", model: { provider: "openai", name: "gpt-4o-mini", temperature: 0.7, maxTokens: 4096, }, tools: ["entity.query"], }) ``` ### Google Provider ```typescript import { defineAgent } from 'struere' export default defineAgent({ name: "Gemini Agent", slug: "gemini-agent", version: "0.1.0", systemPrompt: "You assist with general queries.", model: { provider: "google", name: "gemini-1.5-flash", temperature: 0.7, maxTokens: 4096, }, tools: ["entity.query"], }) ``` ## Required Environment Variables Set the appropriate API key on your Convex deployment depending on which providers your agents use: | Variable | Provider | Required | |----------|----------|----------| | `ANTHROPIC_API_KEY` | Anthropic | Yes (default provider) | | `OPENAI_API_KEY` | OpenAI | Only if using OpenAI models | | `GOOGLE_API_KEY` | Google | Only if using Google models | ## Token Usage Token usage is tracked per interaction and returned in the chat API response: ```json { "usage": { "inputTokens": 1250, "outputTokens": 45, "totalTokens": 1295 } } ``` Each tool call within the agent's LLM loop counts toward token usage. Multi-agent conversations (via `agent.chat`) track usage independently per agent in the chain. All usage is recorded in the `executions` table for monitoring and billing. --- ## Project Structure > Organization-centric project layout and configuration Struere uses an **organization-centric** project layout where all agents, entity types, roles, triggers, and tools are defined as code in a single project directory. The CLI watches these files and syncs them to the Convex backend. ## Directory Layout ``` my-org/ ├── struere.json ├── agents/ │ ├── scheduler.ts │ ├── support.ts │ └── coordinator.ts ├── entity-types/ │ ├── teacher.ts │ ├── student.ts │ ├── guardian.ts │ ├── session.ts │ ├── payment.ts │ └── entitlement.ts ├── roles/ │ ├── admin.ts │ ├── teacher.ts │ └── guardian.ts ├── triggers/ │ └── notify-on-session.ts └── tools/ └── index.ts ``` ## Directory Descriptions ### agents/ Each file exports a single agent definition using `defineAgent`. The file name is conventionally the agent's slug, though the slug is determined by the `slug` field in the definition. ```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: { provider: "anthropic", name: "claude-sonnet-4" }, tools: ["entity.create", "entity.query", "event.emit"], }) ``` ### entity-types/ Each file exports a single entity type definition using `defineEntityType`. Entity types define the schema for structured data that agents can create, query, and manage. ```typescript import { defineEntityType } from 'struere' export default defineEntityType({ name: "Teacher", slug: "teacher", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string", format: "email" }, subjects: { type: "array", items: { type: "string" } }, hourlyRate: { type: "number" }, }, required: ["name", "email"], }, searchFields: ["name", "email"], }) ``` ### roles/ Each file exports a single role definition using `defineRole`. Roles include policies (allow/deny rules), scope rules (row-level security), and field masks (column-level security). ```typescript import { defineRole } from 'struere' export default defineRole({ name: "teacher", description: "Tutors who conduct sessions", 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.guardianNotes", maskType: "hide" }, ], }) ``` ### triggers/ Each file exports a single trigger definition using `defineTrigger`. Triggers define automations that fire when entities are created, updated, or deleted. ```typescript import { defineTrigger } from 'struere' export default defineTrigger({ name: "Notify on New Session", slug: "notify-on-session", on: { entityType: "session", action: "created", condition: { "data.status": "scheduled" }, }, actions: [ { tool: "entity.get", args: { id: "{{trigger.data.teacherId}}" }, as: "teacher", }, { tool: "event.emit", args: { eventType: "session.notification", entityId: "{{trigger.entityId}}", payload: { teacher: "{{steps.teacher.data.name}}" }, }, }, ], }) ``` ### tools/ Contains a single `index.ts` file that exports all custom tool definitions using `defineTools`. These tools are shared across all agents in the organization. ```typescript import { defineTools } from 'struere' export default defineTools([ { name: "send_email", description: "Send an email to a recipient", parameters: { type: "object", properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, }, required: ["to", "subject", "body"], }, handler: async (args, context, fetch) => { const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: [{ to: [{ email: args.to }] }], from: { email: "noreply@example.com" }, subject: args.subject, content: [{ type: "text/plain", value: args.body }], }), }) return { success: response.ok } }, }, ]) ``` ## struere.json The root configuration file identifies the organization and project version. Created automatically by `struere init`. ```json { "version": "2.0", "organization": { "id": "org_abc123", "slug": "acme-corp", "name": "Acme Corp" } } ``` | Field | Type | Description | |-------|------|-------------| | `version` | `string` | Configuration schema version. Currently `"2.0"`. | | `organization.id` | `string` | The Clerk organization ID | | `organization.slug` | `string` | The organization's URL slug | | `organization.name` | `string` | The organization's display name | ## Credentials File Authentication credentials are stored at `~/.struere/credentials.json` after running `struere login`. This file is user-specific and should not be committed to version control. ```json { "token": "eyJhbGciOiJSUzI1NiIs...", "refreshToken": "...", "expiresAt": 1710500000000 } ``` ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `STRUERE_CONVEX_URL` | `your-deployment.convex.cloud` | Convex deployment URL for API calls | | `STRUERE_API_KEY` | — | API key for production deployments | | `STRUERE_AUTH_URL` | `app.struere.dev` | Authentication callback URL | ## Database Schema Overview The Convex backend stores all platform data across the following table categories: | Category | Tables | Description | |----------|--------|-------------| | **User & Org** | `organizations`, `users`, `userOrganizations`, `apiKeys` | User accounts, organizations, memberships, and API keys (env-scoped) | | **Agents** | `agents`, `agentConfigs` | Agent definitions (shared) and environment-specific configurations | | **Conversation** | `threads`, `messages` | Conversation threads and message history | | **Business Data** | `entityTypes`, `entities`, `entityRelations` | Structured data types, instances, and relations (all env-scoped) | | **Events & Audit** | `events`, `executions` | Event log and agent execution tracking (env-scoped) | | **Triggers** | `triggers`, `triggerRuns` | Automation rules and execution records (env-scoped) | | **RBAC** | `roles`, `policies`, `scopeRules`, `fieldMasks`, `toolPermissions`, `userRoles`, `pendingRoleAssignments` | Access control definitions (roles are env-scoped) | | **Integrations** | `integrationConfigs`, `whatsappConnections`, `whatsappMessages`, `providerConfigs`, `calendarConnections` | External service configurations and integration data | | **Billing** | `creditBalances`, `creditTransactions` | Organization credit balances and transaction history | | **Evals** | `evalSuites`, `evalCases`, `evalRuns`, `evalResults` | Agent evaluation and testing | ### Environment-Scoped vs Shared Tables Most tables are scoped by environment, meaning development and production data is fully isolated: **Environment-scoped:** `entityTypes`, `entities`, `entityRelations`, `roles`, `agentConfigs`, `threads`, `messages`, `events`, `executions`, `triggerRuns`, `apiKeys`, `integrationConfigs`, `whatsappConnections` **Shared across environments:** `agents`, `users`, `organizations`, `userOrganizations`, `toolPermissions` The `agents` table stores the agent name, slug, and description which are shared. The environment-specific configuration (system prompt, model, tools) lives in `agentConfigs`, looked up via the `by_agent_env` index on `agentId` and `environment`. ## CLI Sync Mechanism When you run `struere dev`, the CLI: 1. Loads all resource files from `agents/`, `entity-types/`, `roles/`, `triggers/`, and `tools/` 2. Builds a sync payload containing all definitions 3. Sends the payload to the Convex backend via the `syncOrganization` mutation 4. Watches all directories with chokidar for file changes 5. Re-syncs on any file change (add, modify, or delete) The sync payload structure: ```typescript { agents: AgentConfig[] entityTypes: EntityTypeConfig[] roles: RoleConfig[] triggers: TriggerConfig[] } ``` Resources are upserted by slug or name, so renaming a slug creates a new resource rather than updating the existing one. The `dev` command syncs to the **development** environment. Use `struere deploy` to promote to **production**.