Skip to main content

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.

FieldRequiredDescription
targetyesOne of dataset, query, datasource, integration — the resource type this slot binds to
descriptionnoAuthor-facing copy shown in the install/rebind UI
runAsnouser (default) or owner. owner bypasses the viewing user's permissions — use sparingly
optionsnoPer-target options (dataset filters, integration headers, etc.) — validated against the driver's schema
defaultBindingnoA 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 must be a UUID

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 queries
  • GET /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.

Legacy access blocks

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:

  1. Takes a snapshot first (app_snapshot row labeled modernize) — reversible via the returned snapshotId.
  2. For each entry under access.datasets / access.queries / access.datasources / access.integrations, generates a dependencies: slot with a UUID defaultBinding resolved at rewrite time.
  3. Keeps access.apis intact — raw paths don't have a slot model.
  4. 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
FieldRequiredDescription
idYesString identifier used in code
nameYesDisplay name shown in share dialogs
descriptionNoHelp 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 definedwindow.__INFORMER__.roles is []

Variable Reference

Variables are expanded server-side, keeping sensitive values secure.

VariableDescription
$user.usernameLogin name
$user.emailEmail address
$user.displayNameFull name
$user.custom.xxxCustom user field value
$tenant.idTenant identifier
$tenant.<setting>Tenant setting value (from system settings)
$report.idApp UUID
$report.nameApp display name
Missing Variables

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

FieldTypeDescription
descriptionstringHuman-readable description
instructionsstringSystem prompt for the AI model
toolsstring[]App tool names (from tools/ directory)
toolkitsstring[]Toolkit natural IDs to attach
assistantsstring[]Assistant natural IDs — instructions merge into the agent's system prompt
onstring | string[]Event name(s) that trigger this agent
cronstringCron schedule (5-field: min hour dom mon dow)
webSearchbooleanEnable web search tool
modelstringAI 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

FieldTypeDescription
entrystringPath to HTML file relative to public/
labelstringDisplay name in the widget gallery
sizeobjectGrid dimensions: w (width) and h (height) in grid units
refreshnumberAuto-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