struere-developer
Struere Developer Guide
Struere is a full-stack AI agent platform. You define agents, data types, roles, triggers, and custom tools as TypeScript code — then sync to development and deploy to production.
How to Use This Guide
You are working inside a Struere project. Before writing or modifying any Struere code, ALWAYS fetch the relevant documentation first. Never work from memory alone.
Documentation Routing
When you need to do something, fetch the matching URL before writing code:
Behavioral Rules
ALWAYS follow these rules when working in a Struere project:
-
Fetch docs before writing code. Look up the task in the routing table above and fetch the relevant documentation. Do not write Struere definitions from memory.
-
Ask before assuming. Before creating an agent, ask what it should do, who it serves, and what tools it needs. Before creating a role, ask what access level is needed. Do not guess.
-
Validate after changes. Run
bunx struere syncafter editing resource files to validate and sync to development. Check the output for errors. Usestruere triggers logsto verify trigger executions succeeded. -
Use bun, never npm. For package installs and script execution, always use
bun install,bun run,bunx. -
One export per file. Each file in
agents/,entity-types/,roles/,triggers/exports a single default. Onlytools/index.tsexports all custom tools. -
Slugs are identities. Resources upsert by slug. Renaming a slug creates a new resource — it does not rename the existing one. To rename, delete the old slug and create the new one.
-
Environments are isolated.
struere devsyncs to development + eval.struere deploysyncs to production. Data, configs, and API keys are fully isolated per environment. -
Keep tools under 5 per agent. Agent performance degrades with more tools. If an agent needs more, split into specialist agents connected with
agent.chat.
Silent Failure Gotchas
These mistakes cause no visible errors but produce wrong behavior:
-
PolicyConfig has NO
priorityfield. Deny always overrides allow automatically. Addingprioritydoes nothing. -
Scope rule operators:
eq,neq,in,contains. Usingne(common in other systems) silently fails to match. -
Entity link/unlink uses
fromId/toId. UsingfromEntityId/toEntityIdsilently fails. -
Model IDs use OpenRouter format:
provider/model-name. Writeopenai/gpt-5-mini, notgpt-5-mini. Writeanthropic/claude-sonnet-4, notclaude-sonnet-4. -
Default model is
openai/gpt-5-mini(temperature 0.7, maxTokens 4096) whenmodelis omitted from agent config. -
Field masks use allowlist strategy. New fields added to a data type are hidden by default if a field mask exists for that entity type.
-
Template variables that fail resolve to
[TEMPLATE_ERROR: variableName not found]. Always test prompts withstruere compile-prompt <agent-slug>. -
Custom tool
fetch(4th parameter) is unrestricted — it can reach any domain. However, for structured web scraping with markdown conversion, usestruere.web.fetch()(3rd parameter SDK) instead — it routes through Jina and handles HTML-to-markdown conversion. -
entity.querydefault limit is 100, max is 100. Agents that need more data should paginate or use template-only tools for prompt injection. -
agent.chathas depth limit 3 with cycle detection. A calling B calling A is blocked. Design agent graphs to be shallow. -
Fixture sync deletes all existing entities in eval and recreates from YAML. This is a full reset every sync — not incremental.
-
whatsappOwnedTemplatesis org-scoped, not env-scoped. Templates are shared across environments. -
web.fetchreturnshtmlfield (notcontent) when usingreturnFormat: "html". The default markdown mode returnscontent. HTML mode returnshtml. Always check the right field. -
Trigger execution errors are only visible via
struere triggers logs. Immediate triggers (noschedule) don't createtriggerRunsrecords — they emit events. Usetriggers logs [slug]to see execution history,triggers log <event-id>for details. -
router.transferonly works inside a router context. If an agent callsrouter.transferbut the thread was not created via a router, the tool call throws an error. Only addrouter.transferto agents that are part of a router. -
agent.chatin triggers may show "success" while having internal tool errors. The agent self-corrects, so the step succeeds. Usetriggers log <event-id> --verboseto see the full tool call timeline including retries and errors.
Decision Frameworks
Choosing a Model
| Need | Model |
|---|---|
| Cost-sensitive / high-volume | openai/gpt-5-mini or openai/gpt-5-nano |
| Balanced reasoning | anthropic/claude-sonnet-4 or openai/gpt-5 |
| Complex reasoning | anthropic/claude-opus-4 or openai/o3 |
| Fast + cheap | xai/grok-4-1-fast or google/gemini-2.5-flash |
Fetch https://docs.struere.dev/reference/model-configuration.md for full pricing and options.
Built-in vs Custom Tools
Use built-in tools when:
- Agent needs simple CRUD on entities (entity.create, entity.query, etc.)
- Agent needs to send messages (whatsapp.send, email.send)
- Agent needs calendar or payment operations
Use custom tools when:
- Agent needs multi-step workflows (create + notify + update in one call)
- Agent needs external API calls (custom tool
fetchis unrestricted, or usestruere.web.fetch()for markdown conversion) - Agent needs HTML parsing or web scraping (use
struere.web.fetch({url, returnFormat: "html"})) - Agent needs data transformation or computation
- You want to reduce tool count by consolidating multiple operations
- Automations need to call custom logic (tools are org-level in
customToolstable, no agent registration needed)
Use templateOnly: true on custom tools when:
- You need dynamic data in the system prompt without adding to the runtime tool list
- You want to inject computed context at prompt compile time
- The tool should be available to ALL agents' system prompts automatically (no need to list in any agent's
toolsarray)
Routers vs Multi-Agent with agent.chat
Use routers when:
- You have a shared entry point (e.g., WhatsApp number, widget) serving multiple agents
- You want zero-LLM-cost routing via deterministic rules
- You need sticky sessions (user stays with assigned agent across turns)
- You want automatic handoffs between agents mid-conversation
Use agent.chat when:
- An agent needs to delegate a subtask and get a response back inline
- You need real-time orchestration within a single conversation turn
- The calling agent needs to process the response before replying
Structuring Multi-Agent Systems
- Split agents by audience (customer-facing, admin-facing), not by function (booking, notification)
- Use routers for entry-point routing when multiple agents share a channel
- Use triggers for decoupled async communication between agents (retried, tracked) or cron-based recurring tasks
- Use
agent.chatfor real-time delegation within a conversation - Each agent gets its own system prompt, tools, and permission scope
- Keep the agent graph shallow (max depth 3)
Accessing Thread Context in Agents and Tools
Threads carry channel metadata that agents can use to identify senders and personalize behavior.
In system prompts:
{{threadContext.channel}}— the channel (whatsapp,api,widget,dashboard){{threadContext.params.phoneNumber}}— sender's phone number (auto-populated for WhatsApp){{threadContext.params.contactName}}— sender's WhatsApp display name
In custom tool handlers via the context parameter:
context.channel—"whatsapp" | "api" | "widget" | "dashboard"context.channelParams.phoneNumber— sender's phone numbercontext.threadId— current thread IDcontext.threadContext— custom params from the caller
WhatsApp phone number pattern: When a WhatsApp message arrives, the thread stores channelParams = { phoneNumber, contactName?, lastInboundAt? }. Use context.channelParams.phoneNumber in a custom tool to query the sender's entity (e.g., struere.entity.query({ type: "teacher", filter: { field: "phone", operator: "eq", value: phone } })).
System Prompt Structure
Order your system prompt sections by priority:
- Security — never-do rules, boundaries
- Data integrity — required fields, validation rules
- Intent detection — what the agent should do for different requests
- Conversation flows — multi-turn patterns, handoff rules
Always include {{currentTime}} and {{organizationName}}. Test with struere compile-prompt <agent-slug> before deploying.
Workflow Checklists
Creating a New Agent
- Ask: what should this agent do? Who is the audience? What data does it need?
- Fetch: https://docs.struere.dev/sdk/define-agent.md
- Scaffold:
bunx struere add agent <name> - Define: name, slug, version, systemPrompt, model, tools (keep under 5)
- Include
{{currentTime}}and{{organizationName}}in prompt - Sync:
bunx struere devorbunx struere sync - Test:
struere compile-prompt <slug>thenstruere chat <slug>
Setting Up Permissions
- Ask: who needs access to what? What should be hidden?
- Fetch: https://docs.struere.dev/sdk/define-role.md and https://docs.struere.dev/knowledge-base/how-to-set-up-rbac.md
- Scaffold:
bunx struere add role <name> - Define policies (deny overrides allow, NO priority field)
- Add scope rules for row-level filtering (
eq,neq,in,contains) - Add field masks for column-level hiding (allowlist strategy)
- Sync and test with a development API key
Integrating via Chat API
- Fetch: https://docs.struere.dev/llms-api.txt or https://docs.struere.dev/openapi.yaml
- Create API key in dashboard (Settings > API Keys, select environment)
- Use slug endpoint:
POST /v1/agents/:slug/chatwithBearer sk_dev_...orsk_prod_... - Pass
threadIdfor multi-turn conversations - Pass
externalThreadIdfor idempotent mapping from your system - Handle errors: 401 (bad key), 429 (rate limit), 500 (agent error)
Building a Trigger Automation
- Ask: what should trigger this — an entity event or a cron schedule? What should happen?
- Fetch: https://docs.struere.dev/sdk/define-trigger.md
- Scaffold:
bunx struere add trigger <name> - Define:
on— either{ entityType, action, condition? }for entity triggers or{ schedule, timezone? }for cron triggers (5-field cron expression: minute hour day-of-month month day-of-week) - Actions can chain: use
asto name step results, reference with{{steps.<name>}} - Use
agent.chatfor intelligent processing (LLM-powered filtering, analysis) - Use custom tools for deterministic operations (HTML parsing, data transformation)
- Sync:
bunx struere sync - Test: create an entity that matches the trigger's
oncondition - Debug:
struere triggers logs <slug>to see execution,struere triggers log <event-id> --verbosefor details - Check:
struere triggers listshows last run status + errors
Setting Up a Router
- Ask: which agents need to share an entry point? What determines which agent handles a request?
- Fetch: https://docs.struere.dev/sdk/define-router.md
- Scaffold:
bunx struere add router <name> - Define: name, slug, agents array, mode (
rulesorclassify) - For rules mode: define rules with
conditionsarray androute(zero LLM cost) - For classify mode: provide
classifyModelwith{ model, temperature?, maxTokens? }(LLM-based) - Available rule fields:
phoneNumber,channel,{entityType}.*(any entity type slug as prefix, e.g.,contact.tier,teacher.name,student.grade),time.hour,time.dayOfWeek,message.text,message.type - Available rule operators:
eq,neq,in,contains,regex,gt,lt,exists - Set
fallbackfor unmatched requests - Sync:
bunx struere devorbunx struere sync - Test:
struere chat --router <slug> - Connect to WhatsApp: set
routerIdon a whatsappConnection
Example router definition:
import { defineRouter } from 'struere'
export default defineRouter({
name: "Support Router",
slug: "support-router",
mode: "rules",
agents: [
{ slug: "billing-agent", description: "Handles invoices, payments, and billing questions" },
{ slug: "tech-support-agent", description: "Handles bugs, errors, and technical issues" },
{ slug: "general-agent", description: "General customer support" },
],
rules: [
{
conditions: [
{ field: "message.text", operator: "contains", value: "invoice" },
],
route: "billing-agent",
},
{
conditions: [
{ field: "message.text", operator: "contains", value: "bug" },
],
route: "tech-support-agent",
},
],
fallback: "general-agent",
})
Key behaviors:
- Sticky routing: once a thread is assigned to an agent, subsequent messages go to the same agent until explicitly transferred
router.transfer: built-in tool that agents can call to hand off the conversation to another agent in the router- Transfer count: tracked per thread via
transferCountfield, visible in conversation logs - Rules are evaluated top-to-bottom; first match wins
- Classify mode uses an LLM call to pick the best agent based on agent descriptions
Debugging a Trigger
struere triggers list— check if trigger is enabled, see last run status and errorstruere triggers logs <slug>— see execution history with step counts and timingstruere triggers log <event-id>— see per-step detail: resolved args, results, errorsstruere triggers log <event-id> --verbose— see agent.chat tool call timeline (which tools failed, which succeeded)struere logs view <thread-id> --exec— full agent conversation transcript (thread ID shown in trigger log output)
Common issues:
- Custom tool not found → ensure tool is defined in
tools/index.tsand synced (tools are org-level, not agent-level) - Template variable empty → check field name matches response shape (e.g.,
agent.chatreturnsresponsenotmessage) web.fetchreturns empty content → usereturnFormat: "html"for HTML parsing, checkhtmlfield notcontent
Adding an Integration
- Ask: which integration? (WhatsApp, Calendar, Airtable, Email, Payments)
- Fetch the integration page from the routing table above
- Configure via CLI:
struere integration <provider> --<key> <value> - Add the integration's tools to the agent's tool list
- Sync and test in development first