informer.yaml Reference
The informer.yaml file is the central configuration file for your Magic App. It declares the data your app depends on, custom roles, agents, events, and dashboard widgets.
Place it in your project root. It's uploaded to the app's library on deploy and enforced at runtime by the Informer server.
Basic Structure
# informer.yaml
# Typed slots the installer binds at first deploy. Slot names are
# referenced from server-side handler code as `context.<slot>.<method>(...)`
# and arrive pre-typed.
dependencies:
sales:
target: dataset
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
summary:
target: query
defaultBinding: 9a8b7c6d-5e4f-3a2b-1c0d-fedcba987654
salesforce:
target: integration # no defaultBinding — installer picks
# Raw API allowlist for endpoints that don't fit the typed-slot model.
# Coexists with `dependencies:`; both are honored.
access:
apis:
- POST /api/models/go_everyday/_object
roles:
- id: viewer
name: Viewer
description: Read-only access
Dependencies Section
The dependencies: block is the preferred way to declare data access. Each entry defines a typed slot that the installer binds (or leaves to the manifest's defaultBinding) at deploy time. The slot becomes a property on the handler's context object — call methods on it instead of building raw API URLs.
| Field | Required | Description |
|---|---|---|
target | yes | One of dataset, query, datasource, integration — the resource type this slot binds to |
description | no | Author-facing copy shown in the install/rebind UI |
runAs | no | user (default) or owner. owner bypasses the viewing user's permissions — use sparingly |
options | no | Per-target options (dataset filters, integration headers, etc.) — validated against the driver's schema |
defaultBinding | no | A UUID the slot binds to automatically on first deploy. Look up via the resource's *-list endpoint (e.g. GET /api/datasets-list). Never overwrites a hand-bound slot — re-deploys won't silently rewire installer choices. |
defaultBinding accepts UUIDs only, not configIds (admin:sales-data). UUIDs round-trip cleanly through bundle export/import and survive resource renames. configIds in defaultBinding are rejected at deploy with a clear must be a UUID error.
Calling Slots from Handler Code
Each slot becomes a property on the context object passed to your handlers. Methods depend on the slot's target:
// informer.yaml declared:
// dependencies:
// orders: { target: dataset, defaultBinding: <uuid> }
// summary: { target: query, defaultBinding: <uuid> }
// analytics: { target: datasource, defaultBinding: <uuid> }
// salesforce: { target: integration, defaultBinding: <uuid> }
export async function GET({ context }) {
// dataset → search(esQuery) / fields()
const hits = await context.orders.search({
query: { range: { total: { gte: 100 } } },
size: 50
});
// query → execute(params)
const summary = await context.summary.execute({ month: '2026-05' });
// datasource → query(payload)
const rows = await context.analytics.query({ sql: 'SELECT * FROM events' });
// integration → request(opts)
const sf = await context.salesforce.request({
method: 'GET',
url: '/services/data/v59.0/sobjects/Account/00112233'
});
return { hits, summary, rows, sf };
}
If the installer hasn't bound a slot yet (or the bound target was deleted), the proxy throws a boom 422 with a structured errorCode:
try {
return await context.orders.search({ query: { match_all: {} } });
} catch (err) {
// err.output.payload.data.errorCode is 'dependency_unbound' (slot
// never bound) or 'dependency_broken' (bound target deleted).
if (err.output?.payload?.data?.errorCode === 'dependency_unbound') {
return { status: 503, body: { error: 'This app needs setup — bind the orders dataset.' } };
}
throw err;
}
Target Types
dataset
Grants search(esQuery) and fields() on the bound dataset.
dependencies:
sales:
target: dataset
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
Server-side, this maps to:
POST /api/datasets/{uuid}/_search— Elasticsearch queriesGET /api/datasets/{uuid}/fields— Field metadata
query
Grants execute(params) on the bound saved query.
dependencies:
daily_summary:
target: query
defaultBinding: 9a8b7c6d-5e4f-3a2b-1c0d-fedcba987654
Server-side: POST /api/queries/{uuid}/_execute
datasource
Grants query(payload) on the bound datasource.
dependencies:
postgres_main:
target: datasource
defaultBinding: 3e4f5a6b-7c8d-9e0f-1234-567890abcdef
Server-side: POST /api/datasources/{uuid}/_query
integration
Grants request(opts) on the bound integration (Salesforce, REST APIs, etc.).
dependencies:
salesforce:
target: integration
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
Server-side: POST /api/integrations/{uuid}/request
Row-Level Security (dataset options)
Restrict data based on the viewing user's profile. Filters live under each slot's options:
dependencies:
orders:
target: dataset
defaultBinding: 1f2e3d4c-5b6a-7980-1234-56789abcdef0
options:
filter:
region: $user.custom.region # Users only see their region
sales_rep: $user.username # Users only see their own records
Filters are injected server-side into every search request. The app code doesn't need to handle filtering — it's automatic and tamper-proof.
Credential Injection (integration options)
Pass user-specific credentials to external APIs via the slot's options:
dependencies:
partner_api:
target: integration
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
options:
headers:
Authorization: Bearer $user.custom.partnerToken
X-Client-ID: $tenant.id
params:
user_id: $user.custom.externalId
Headers and params are expanded server-side — sensitive values are never exposed to client JavaScript.
Path Restrictions (integration options)
Limit which endpoints the app can call on an integration:
dependencies:
salesforce:
target: integration
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
options:
paths:
- /data/*/query
- /data/*/sobjects/*
runAs
By default, every dep call runs with the viewing user's credentials. To run as the app owner (bypassing the viewer's permissions), set runAs: owner:
dependencies:
curated_view:
target: dataset
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
runAs: owner # Trusted — bypasses viewer permissions
Use owner sparingly — it's how you deliberately share data the viewer wouldn't otherwise see.
Access Section (Raw API Allowlist)
For API paths that don't fit the typed-slot model — custom endpoints, AI model routes, internal APIs — use the access.apis: block:
access:
apis:
- POST /api/custom/endpoint
- GET /api/special/resource
- POST /api/models/go_everyday/_chat
- POST /api/models/go_everyday/_completion
- POST /api/models/go_everyday/_object
- /api/read-only/resource # METHOD defaults to GET if omitted
Each entry is a string in the format METHOD /api/path. If the HTTP method is omitted, it defaults to GET. Paths support * wildcards for path segments.
You can still declare access.datasets, access.queries, access.integrations, access.datasources, and access.libraries instead of using dependencies:. The runtime extracts those into the same app_entity whitelist, but they don't appear in the install/rebind UI — every change requires editing the YAML and redeploying. For new apps, prefer dependencies: slots.
Modernizing a Legacy access: App
The server provides a one-shot route to convert a legacy access:-based manifest into dependencies: slots:
POST /api/apps/{id}/_modernize-manifest
The route:
- Takes a snapshot first (
app_snapshotrow labeledmodernize) — reversible via the returnedsnapshotId. - For each entry under
access.datasets/access.queries/access.datasources/access.integrations, generates adependencies:slot with a UUIDdefaultBindingresolved at rewrite time. - Keeps
access.apisintact — raw paths don't have a slot model. - Deploys the rewritten manifest.
A failure after the snapshot is taken returns 400 with the snapshotId so you can restore. You'll also need to update any handler code that referenced these resources via raw fetch('/api/datasets/<id>/_search', ...) to use context.<slot>.search(...) instead.
Roles Section
Define custom roles that publishers assign when sharing the app:
roles:
- id: viewer
name: Viewer
description: Can view reports but not take actions
- id: approver
name: Approver
description: Can approve or reject requests
- id: manager
name: Manager
description: Full management access
| Field | Required | Description |
|---|---|---|
id | Yes | String identifier used in code |
name | Yes | Display name shown in share dialogs |
description | No | Help text shown in share dialogs |
Reading Roles
Client-side:
const roles = window.__INFORMER__?.roles || [];
if (roles.includes('approver')) {
showApprovalPanel();
}
Server-side (route handlers):
// Automatic enforcement via config
export const config = {
roles: ['admin', 'manager'] // 403 if user lacks these roles
};
// Manual checking
export async function GET({ request }) {
if (request.roles.includes('manager')) {
// show admin data
}
}
How Roles Are Assigned
- Internal shares — Publisher selects roles in the share dialog
- External links — Publisher selects roles when creating the link; roles are baked into the token
- Publisher+ on owning team — Automatically receives all defined roles
- No roles defined —
window.__INFORMER__.rolesis[]
Variable Reference
Variables are expanded server-side, keeping sensitive values secure.
| Variable | Description |
|---|---|
$user.username | Login name |
$user.email | Email address |
$user.displayName | Full name |
$user.custom.xxx | Custom user field value |
$tenant.id | Tenant identifier |
$tenant.<setting> | Tenant setting value (from system settings) |
$report.id | App UUID |
$report.name | App display name |
If a variable references a custom user field that doesn't exist (e.g., $user.custom.region when the user has no region field), it resolves to an empty string. For dataset filters, this means the filter matches records where that field is empty — not all records. Ensure all referenced custom fields exist for your users.
Agents Section
Define AI agents that execute autonomously in response to events, cron schedules, or manual triggers. See the Agents API docs for full details.
agents:
order-processor:
description: Processes new orders and sends confirmations
instructions: |
You are an order processing agent. When triggered by an order_created event,
verify the order details, update inventory, and send a confirmation email.
tools:
- send-email
- update-inventory
on: order_created
model: go_everyday
daily-report:
description: Generates a daily summary email
instructions: Summarize today's orders and email the report to the team.
tools:
- send-email
cron: "0 17 * * 1-5"
crm-agent:
description: Customer support with CRM tools
instructions: Help customers using CRM data and web search.
toolkits:
- admin:crm-toolkit
assistants:
- admin:support-persona
on: support_request
webSearch: true
Agent Fields
| Field | Type | Description |
|---|---|---|
description | string | Human-readable description |
instructions | string | System prompt for the AI model |
tools | string[] | App tool names (from tools/ directory) |
toolkits | string[] | Toolkit natural IDs to attach |
assistants | string[] | Assistant natural IDs — instructions merge into the agent's system prompt |
on | string | string[] | Event name(s) that trigger this agent |
cron | string | Cron schedule (5-field: min hour dom mon dow) |
webSearch | boolean | Enable web search tool |
model | string | AI model slug (default: go_everyday) |
Toolkits
Agents can reference external toolkits that provide additional tools. Toolkit references are resolved at deploy — if a referenced toolkit doesn't exist, deploy fails with a clear error.
agents:
my-agent:
instructions: Use CRM tools to look up customer data.
toolkits:
- admin:crm-toolkit # Resolved by naturalId at deploy
on: customer_inquiry
At execution time, the agent receives all tools from its referenced toolkits alongside its app tools. Toolkit system instructions are appended to the agent's prompt.
Assistants
Agents can reference Informer assistants to inherit their instructions. Each assistant's system prompt is merged into the agent's own instructions at execution time.
agents:
support-bot:
instructions: Handle customer inquiries.
assistants:
- admin:support-persona # Assistant instructions merged in
on: support_request
Assistant references are validated at deploy time — if a referenced assistant doesn't exist, deploy fails with a clear error. At runtime, assistant instructions are prepended to the system prompt before toolkit instructions.
Cron Schedules
Standard 5-field cron format: minute hour day-of-month month day-of-week
agents:
nightly-sync:
instructions: Sync data from external system.
cron: "0 3 * * *" # 3:00 AM daily
weekly-report:
instructions: Generate weekly report.
cron: "0 9 * * 1" # 9:00 AM every Monday
frequent-check:
instructions: Check for new items.
cron: "*/15 * * * *" # Every 15 minutes
Events Section
Declare event types that agents can listen for. Events are emitted from server route handlers via the emit() callback.
events:
order_created:
description: Fired when a new order is submitted
order_shipped:
description: Fired when an order ships
support_request:
description: Fired when a customer opens a support ticket
agents:
order-processor:
on: order_created
instructions: Process the new order.
support-bot:
on: support_request
instructions: Respond to the support ticket.
Event types are stored in app.defn.events on deploy. To emit an event from a server route:
export async function POST({ query, emit, request }) {
const [order] = await query(
'INSERT INTO orders (customer, total) VALUES ($1, $2) RETURNING *',
[request.body.customer, request.body.total]
);
await emit('order_created', { orderId: order.id });
return { status: 201, body: order };
}
Widgets Section
Declare dashboard widgets that render inside Informer's widget gallery. Widgets are static HTML files served in iframes at fixed grid sizes.
widgets:
cash-balance:
entry: widgets/cash-balance.html
label: Cash Balance
size: { w: 2, h: 1 }
refresh: 300
sales-trend:
entry: widgets/sales-trend.html
label: Sales Trend
size: { w: 2, h: 2 }
refresh: 300
Widget Fields
| Field | Type | Description |
|---|---|---|
entry | string | Path to HTML file relative to public/ |
label | string | Display name in the widget gallery |
size | object | Grid dimensions: w (width) and h (height) in grid units |
refresh | number | Auto-refresh interval in seconds |
Widget HTML files go in public/widgets/ and are self-contained (inline CSS/JS, no external dependencies). They use the same dependencies: and access: declarations as the main app.
Complete Example
# informer.yaml
dependencies:
sales:
target: dataset
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
orders:
target: dataset
defaultBinding: 1f2e3d4c-5b6a-7980-1234-56789abcdef0
options:
filter:
region: $user.custom.region
monthly_summary:
target: query
defaultBinding: 9a8b7c6d-5e4f-3a2b-1c0d-fedcba987654
salesforce:
target: integration
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
sendgrid:
target: integration # installer picks at bind time
access:
apis:
- POST /api/models/go_everyday/_object
roles:
- id: viewer
name: Viewer
description: Read-only access
- id: manager
name: Manager
description: Full management access
events:
order_created:
description: New order submitted
order_shipped:
description: Order has shipped
agents:
order-processor:
description: Processes new orders
instructions: Verify order details and update inventory.
tools:
- update-inventory
- send-email
on: order_created
daily-digest:
description: Daily order summary
instructions: Summarize today's orders and email to managers.
tools:
- send-email
cron: "0 17 * * 1-5"
widgets:
order-count:
entry: widgets/order-count.html
label: Today's Orders
size: { w: 2, h: 1 }
refresh: 60