RylvoRylvo
API Documentation

Rylvo API Reference

One endpoint. Full agentic control. Run a bot turn — prompts, guardrails, tools, and knowledge-base retrieval — on your own LLM key, in minutes.

Quick Start

Make your first API call in under a minute. You need a botId and an API key from the dashboard. API keys are included from the Lite plan; Free and Starter workspaces have none, and put a bot on a website with the widget or share it with a Beam link instead.

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "botId": "your_bot_id",
    "sessionId": "session_001",
    "message": "I can't access my account."
  }'

Access Model

Rylvo exposes three surfaces. Knowing which one to use for what avoids confusion when integrating a backend, connecting an AI editor, or operating the platform day-to-day.

Path convention: Firestore document paths in this reference write {envRoot} as shorthand for the workspace-environment root — the environments/{environment} subtree under organizations/{orgId} — where {environment} is production or test. Environment-owned collections exist independently per environment — the same logical ID in Test and Production refers to two different documents. Workspace-wide data (members, billing, audit log) stays directly under organizations/{orgId}.

Public RESTBackend runtime

Call from your backend with your org API key (Authorization: Bearer rylv_…). This is the runtime surface you ship in production.

  • POST /api/v1/bot-run (scope: respond)
  • POST /api/v1/kb/query (scope: kb:read)
  • POST /api/v1/kb/documents (scope: kb:write)
  • POST /api/v1/tests/gate (scope: tests:read)
  • /health, /ready, /live, /flags (unauthenticated)
MCP / AI EditorExternal AI tools

Connect Claude Code, Cursor, Windsurf, Zed, or Codex directly to Rylvo. OAuth 2.1 + PKCE, org-scoped tokens.

  • POST /mcp (Streamable HTTP)
  • /oauth/* registration + token exchange
  • 167 tools for bots, prompts, KB, skills, connectors
  • OpenAPI 3.1 spec of the 3 public REST endpoints at /openapi.json
DashboardConfiguration & ops

Manage from rylvo.com/dashboard. Many features are also reachable through the MCP / AI Editor surface.

  • Bots, Prompts, Guardrails, Knowledge Base
  • Workspace Architect, Bot Chat, Bot Tests
  • Edge Cases, Red Team, Automation, Reports
  • Mission Control, Agent Evolution, Skills, MCP Hub
  • Billing, Team, API Keys, Build Intelligence

Sections below marked with Dashboard-only do not yet have a dedicated public REST endpoint. They remain fully functional inside the dashboard, through the MCP / AI Editor surface, and their behavior affects the /api/v1/bot-run runtime.

Environments & Releases

Every workspace has two environments: Test and Production. You build and experiment in Test, then promote a frozen, versioned release to Production. Configuration, data, analytics, traces, and API keys are isolated per environment — the same logical ID in Test and Production refers to two different documents (see Access Model for the {envRoot} path convention). There is a dedicated Environments & Releases overview if you want the product tour.

Testeditable

A fully editable copy of your workspace. Iterate on bots, prompts, guardrails, flows, skills, knowledge, and connectors against real models. Test traffic never touches Production configuration or data.

Productionread-only

What your customers reach. Promotable configuration is read-only — it only changes when you activate a release or roll one back. An org that has never promoted keeps serving its legacy configuration until the first release is activated.

The release lifecycle

  1. Freeze a candidate — snapshot the current Test configuration into an immutable release candidate (a versioned manifest of artifacts). Test stays editable.
  2. Review the diff & gates — see what changed versus live Production (added / changed / removed / unchanged, grouped by artifact family). Optional gates: two-person approval (the creator cannot approve their own manifest) and a green test run.
  3. Activate Production — the active pointer flips atomically, but only after every artifact in the manifest is verified. The environment generation increments by one.
  4. Roll back if needed — every activated release is immutable and retained. Point Production back to any earlier release and runtime traffic switches immediately.

You can also refresh Test from the active Production release (safe mode preserves divergent Test edits) or reset Test entirely. Every destructive operation is checkpointed first. Managing releases (freeze, activate, rollback) requires an admin or owner role.

Selecting an environment over the API

Send the X-Rylvo-Environment header to choose the environment for a request. Its value is production or test. API keys are also scoped to an environment, so a Test key can never authenticate against Production data. The same bot answers from the right configuration and writes to the right data store for the selected environment.

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Authorization: Bearer rylv_…" \
  -H "X-Rylvo-Environment: test" \
  -H "Content-Type: application/json" \
  -d '{"botId": "…", "message": "hello"}'

Omitting the header defaults to Production. See Authentication and API Keys for how keys are scoped.

Test-data watermarking

Data exported from Test is watermarked so a Test dataset is never mistaken for genuine customer records downstream. Production exports carry no watermark; Test exports always do, and the environment provenance travels with the data after it leaves Rylvo.

Authentication

All API requests require an API key sent in the X-API-Key header. Create keys from your dashboard.

http
X-API-Key: rylv_abc123...

Security: Never expose API keys in client-side code. Always call the API from your backend.

BotsDashboard-only

Rylvo Bots walkthrough

Every agent in your workspace is built, configured, and shipped from Dashboard → Bots. The sidebar lists agent groups first and standalone bots after, each showing the model it runs on and whether it is live.

New Bot — from scratch

  • Name, generated avatar, and a description of what the agent is for
  • Model, chosen once from every provider you hold a key for (see BYOK below)
  • Image and audio input toggles
  • How many recent messages the bot rereads each turn
  • Optionally drop the new agent straight into a group

From a template

  • Catalog of validated blueprints, searchable and filterable by category
  • Single-agent bots first, multi-agent pipelines after
  • Choosing one opens a customize step — nothing is created until you finish it
  • What you get is a fully editable bot, not a locked preset

A bot is only reachable over the API once it is live and you hold a provider key for its model. Group several bots together so they can hand work to one another — see Agent Groups.

Provider Keys (BYOK)Dashboard-only

Every bot chat on Rylvo runs on your own LLM provider key. Rylvo routes the call server-side and logs a full trace for observability, but adds zero markup on token costs — you pay your LLM provider directly. Manage keys in Settings → LLM Provider Keys.

Bot Chat, Group Chat, Bot Tests

Route to the customer's own provider key. Requires either the matching direct-provider key (e.g. Anthropic for anthropic/* models) or an OpenRouter universal key. Without a key the UI blocks the chat and surfaces a CTA. New workspaces start with a 14-day trial key carrying $2 of AI usage, so they can chat before adding one.

Workspace Architect

Uses Rylvo's own server-side key - you don't need to add anything to run the Architect. Only the bots it creates need BYOK before you can chat with them.

Key resolution order

  1. Auto (default) - the model's native provider key when added (direct to e.g. api.anthropic.com), otherwise your OpenRouter key.
  2. Always OpenRouter - every chat runs on your OpenRouter key, even when a direct key exists.
  3. Native keys only - OpenRouter is never used; models without a direct key are locked.
  4. Per-bot override available in the Bot Hub. When no key can serve a model, the chat UI names exactly which key(s) would unlock it.

Supported providers

  • OpenRouter - universal, covers every model except Groq's and GLM 4.7 FlashX
  • OpenAI - openai/*
  • Anthropic - anthropic/* (native Messages API adapter)
  • Google Gemini - google/* (native generateContent adapter)
  • DeepSeek - deepseek/*
  • xAI (Grok) - x-ai/*
  • Kimi / Qwen / GLM - moonshotai/*, qwen/*, z-ai/* (older bots: kimi/*, zhipu/*); keys from either region work
  • Mistral - mistralai/*
  • Groq - groq/* (needs its own key; not on OpenRouter)
  • MiniMax, Meta, Cohere, NVIDIA, Xiaomi, ByteDance, Tencent, StepFun, Inception, Upstage, IBM - OpenRouter key only
  • The full model list, with prices and the key each runs on, is in the dashboard model picker (Bots → Edit → Model).

Access & security

  • Write/delete restricted to org admins.
  • Read allowed for all org members so operators can chat with bots.
  • Keys never leave Firestore + the route handler - never exposed to page HTML.
  • Key rotation automatically resets verification state.

Verified when saved

Every key is checked with its provider before it is stored - a 16-token ping (< $0.001). A key the provider rejects is never saved, and the page shows exactly what the provider answered. Use Test to re-check a saved key any time and Test all to re-verify every key at once.

Bot Tagging (Mandatory)

Required: Every runtime request must include a botId (or a groupId for an agent group). Requests with neither are rejected with a 400 error.

Bots are the primary organizational unit for your API usage. Each bot represents a distinct agent or use case (e.g. "Support Triage Bot", "Sales Qualifier Bot"). Create bots from your dashboard, then use the bot ID to tag every API call.

How it works

1

Create a bot

Go to Dashboard → Bots → New Bot. Give it a name and description.

2

Copy the bot ID

Each bot gets a unique ID. Copy it from the bot card.

3

Tag every API request

Include "botId": "your_bot_id" in the request body.

4

View per-bot analytics

Filter traces, analytics, and billing by bot.

Example request with botId

json
{
  "botId": "bot_abc123def456",
  "sessionId": "sess_001",
  "message": "I can't access my account."
}

Benefits of bot tagging

Per-bot analytics

View metrics, latency, and error rates per bot

Trace grouping

Filter and search traces by bot name

Key association

Link API keys to specific bots

Access control

Manage permissions per bot

Bot Chat PlaygroundDashboard-only

Rylvo Bot Chat Playground walkthrough

Test a bot's behavior end-to-end before pointing production traffic at it. The playground at /dashboard/bots/[botId]/chat assembles the full runtime exactly like /api/v1/bot-run does: system prompts, placeholders, guardrails, connectors, KB connections, and evolution rules.

Every turn captures

  • Active prompt IDs + resolved placeholders
  • Guardrails evaluated (and which triggered)
  • Connectors invoked + request/response bodies
  • Evolution rules injected into the system prompt
  • Active flow step + transitions evaluated (if a flow is attached)
  • Token usage + cost (runs on your own BYOK key)

Good for

  • Reproducing a failure from a trace replay
  • Iterating on a prompt before promoting to active
  • Verifying a new guardrail actually fires
  • Comparing two bots side-by-side (/dashboard/bots/[botId]/compare)

Each playground turn shows the prompt stack, guardrail decisions, connector calls, evolution rules applied, the active flow step (if a flow is attached), operator whispers, token usage, and cost - all alongside the message itself.

Bot TestsDashboard-only

Rylvo Bot Tests walkthrough

Define reusable test cases per bot and run them on demand or via an Automation schedule. Manage at /dashboard/bots/[botId]/tests.

A test case specifies

  • Input message (single-turn) or a scripted multi-turn conversation
  • Expected output text / behaviour
  • Guardrails that should or must not trigger
  • Checks on the replies: words they must or must never use, a pattern, JSON of a given shape, a number within a margin, wording close to a reference answer
  • Checks on what the bot did: the tools it called (or must never call, or in what order), whether it used the knowledge base, how fast every reply came, what the conversation cost
  • Optional pre-populated structured_state

A test run produces

  • Pass/fail per case with diff against expectations
  • Token usage & cost for the whole run
  • Full trace links for every case (for debugging)
  • A summary you can attach to a report or alert channel

How a test is scored is yours. Tests → Scoring sets the pass mark (70 unless you change it), stricter bars for a priority or kind of test, what the AI judge scores and how much each part counts — for every bot, or for one. Test re-scores the bot's last run before you save, and every run records the scoring it was judged under, so runs scored differently are never compared as if nothing changed. Every plan can see it; changing it is Pro, for admins and owners.

Checks are hard rules. Each one fails the test on its own, whatever the AI judge scores, and a check with nothing to read — a reply time that wasn't recorded, a bot with no knowledge base — fails and says so instead of passing quietly. Similarity compares wording, not meaning: use it where an exact phrase is wrong to demand but the answer should read much the same.

A failure becomes a test. On an edge case, or any reply in Traces, "Add as test" opens the test editor already filled in: the customer's messages up to where it went wrong, what should have happened, and a check that keeps it from passing again. Save it and the same bug can't ship twice — the edge case lists the tests made from it, and runs them.

Test sets keep a suite fixed. Save the tests that matter — say, the ones that must pass before a release — as a named set. Each version freezes a copy of every test in it, so "the release suite, version 3" runs the same conversations against the same expectations in March and in December, however the tests are edited since. Each version marks which of its tests have changed or been deleted since, and every run says which version it ran.

Tests travel as files. Download a bot's tests, or one version of a set, as CSV (one test per row, for a spreadsheet), JSON Lines or JSON — and import any of them back. An import is checked before anything is added, and a file that can't be read says which row and why.

How far to trust the judge (Enterprise). Reviewers give their own verdict on judged results — pass, partial or fail, and a score for any part if they like. Tests → Scoring then shows how often the AI judge agrees with them (with Cohen's kappa, so agreement by chance doesn't count), whether it is more often too generous or too strict, and which part it scores furthest from your reviewers. Testing a scoring change says whether the judge would agree with them more, and a pinned baseline judge flags any run judged differently — its scores don't compare with the trend.

A release waits until the tests pass. Switched on in Tests → Release gate, a release to Production waits until every bot in it clears the gate on a run against that exact release, and Review & release names the tests that didn't pass, each a link to its result. Unless you change it, a run clears the gate when none of its tests fail and at least one passes. On Enterprise you decide what passing means — a pass rate, every critical test, a run of a named test set, the judge's agreement with your reviewers — and your own CI pipeline asks the same gate with POST /api/v1/tests/gate and an API key with the tests:read scope, failing the deploy unless it says passing.

Failed test cases can be converted into Edge Cases so the engine tracks them through the detect → classify → fix → verify loop.

Agent Groups (Multi-Bot Teams)Dashboard-only

Compose multiple bots into a team where one bot routes, delegates, and merges results from specialists. Useful for workflows that span domains (e.g. billing + technical support) or require parallel drafts.

Group modes

  • Router - one bot picks the best specialist per turn
  • Sequential - bots run in a fixed pipeline
  • Parallel - bots run simultaneously, a merger summarises
  • Round-robin - alternates bots across turns

Each member contributes

  • Its own prompts, guardrails, connectors, KB scope
  • An attached role/persona (e.g. billing_specialist)
  • Its own trace - the group trace links all of them

Group chats record which member bots contributed to each turn and how routing decisions were made, so you can debug who said what and why.

Typed decisions in groups: a router whose rules read only the intent (no other facts, no pass_fields) is dispatched by TypeSafe Jev; other routers keep the LLM classifier. A fan-out with the vote merge has Jev pick the best branch, and that branch's reply is returned exactly as written — previously an LLM re-typed it. Both fall back to the LLM path if Jev cannot answer.

Endpoints

MethodPathDescription
POST/api/v1/bot-runRun one bot or agent-group turn (scope: respond)
POST/api/v1/kb/queryQuery a knowledge-base connection (scope: kb:read)
POST/api/v1/kb/documentsUpload documents to a KB source (scope: kb:write)
POST/api/v1/tests/gateDoes a bot's test run clear the release gate? For CI (scope: tests:read)
GET/healthLiveness (process alive)
GET/readyReadiness (can handle requests)
GET/liveUptime probe
GET/flagsRollout flags

The model catalog, traces, analytics, and connector CRUD are managed from the dashboard and the MCP / AI Editor surface — they are not exposed as public REST endpoints today.

Runtime (Bot Turn)

POST /api/v1/bot-run is the core of Rylvo. Send a message (plus optional session + history) and the engine runs one full agentic turn, returning the assistant reply with full telemetry. It runs on your own BYOK key.

Prompt assembly

Bot prompts + skills

Guardrails

Block / rewrite / escalate

Tools & connectors

Live tool calls

KB retrieval

Grounded answers

Request Format

json
{
  "botId": "bot_abc123def456",
  "sessionId": "sess_001",
  "message": "The display shows error E42.",
  "conversationHistory": [
    { "role": "user", "content": "Hi, my device won't start." },
    { "role": "assistant", "content": "I can help. What does the screen show?" }
  ],
  "endUserExternalId": "user_8842",
  "endUserDisplayName": "Jordan Lee",
  "channel": "api"
}

Required Fields

FieldTypeDescription
botIdstringBot identifier (from Dashboard → Bots). Either botId or groupId is required.
messagestringThe end user's current message

Optional Fields

FieldTypeDescription
groupIdstringAgent-group id (run a multi-bot team instead of a single bot)
conversationHistoryobject[]Prior turns: { role: 'user' | 'assistant', content }
sessionIdstringConversation/session id; echoed back as conversationId
endUserExternalIdstringStable end-user id in your namespace (≤256). Required for display name / email.
endUserDisplayNamestringHuman-readable name shown in Traces (≤200)
endUserEmailstringEnd-user email for support handoff (validated)
known_factsobject[]What you already know about the person: [{ key, value }], at most 20. key is a Data Collection field key of the bot (lower case, ≤64, sent once); value is text ≤1000. Used only so the bot doesn't ask again; never saved; it must be a valid answer to that field. A malformed list is a 400.
channelstringapi (default) | widget | whatsapp | slack | unknown
modelstringOverride model (e.g. openai/gpt-4.1-mini, anthropic/claude-sonnet-4.6)
maxTokensnumberMax output tokens for this turn
temperaturenumberSampling temperature for this turn
disableToolsbooleanDisable tool/connector calls for this turn
maxToolIterationsnumberCap the tool-call loop iterations
responseFormatobjectStructured output: {type:"json_object"} or {type:"json_schema", json_schema:{...}} — constrains the final reply (OpenAI-compatible models only)
streambooleanToken streaming via Server-Sent Events: token events as the reply generates, then a final result event with the standard JSON body (single-bot only)

Optional header Idempotency-Key: <uuid> — re-sending the same key for the same API key replays the stored response (no double-billing / re-running tools).

Response Format

json
{
  "response": "I've logged error E42. Let me pull up the matching fix steps.",
  "conversationId": "sess_001",
  "runId": "run_8f3a21c9",
  "model": "openai/gpt-4.1-mini",
  "latencyMs": 1840,
  "tokens": { "prompt": 1203, "completion": 88, "total": 1291 },
  "guardrails": {
    "inputBlocked": false,
    "outputBlocked": false,
    "outputRewritten": false,
    "blockedBy": null,
    "escalated": false,
    "escalatedBy": null
  },
  "toolCalls": [],
  "augmentation": { "rulesApplied": 0, "skillsMatched": 1, "episodicRecallCount": 0, "userModelApplied": false },
  "sessionEnd": null,
  "kb": null,
  "pendingApproval": null
}

Key Response Fields

FieldDescription
responseThe assistant's reply text to show the user
runIdUnique id for this turn — find it in Dashboard → Traces
conversationIdEchoes the request sessionId
modelThe model that actually served the turn
latencyMsServer-side turn latency (ms)
tokens{ prompt, completion, total } token counts
guardrails{ inputBlocked, outputBlocked, outputRewritten, blockedBy, escalated, escalatedBy } — inputBlocked = the message was refused before the model ran; outputBlocked = the reply was blocked after it was generated
toolCallsTools invoked this turn (name, disposition, latency, cost)
sessionEndSet when the bot ended the session ({ reason, summary? })
kbKB grounding telemetry incl. citations (when retrieval ran)
pendingApprovalPresent when a tool call awaits operator approval

Flows

Rylvo Flows conversation graph

Flows are an optional guided/strict conversation graph attached to a bot (bot.flowId) or an agent group (dashboard/MCP-managed) that shapes its runtime behaviour — they are not part of the /api/v1/bot-run request or response. A flow is a set of steps: the engine keeps a per-session pointer to the current step, injects that step's goal into the system prompt, scopes its tools, and advances when a transition fires.

A step carries

goalInjected into the system prompt as this step's instruction
collectInformation to ask for before moving on — the flow keeps what the bot can't save
allowed_toolsStrict: the only tools offered. Guided: named to the bot as guidance
transitionsEdges to the next step, evaluated every turn

Transition kinds

alwaysFires unconditionally
data_completeAll of the step's collect fields are gathered
intentThe classified intent matches
conditionAn expression over the fields (null until given), tracked details as state.<key>, and turn_count / step_turns is true
tool_resultA named tool ran successfully this turn

How intent is classified: when a step has intent transitions, TypeSafe Jev picks one of that step's intents. A transition is taken only when Jev is at least 50% sure; otherwise the flow stays on its step. If Jev cannot answer, the previous LLM classifier on the bot's model decides.

Bot workflow stages (bot.workflowId, separate from flows): when the workflow defines two or more stages, the engine picks the current stage each turn from that workflow's own stage ids and scores the user's sentiment from -1 (furious) to 1 (delighted) in the same Jev request. sentiment < … conditions in the workflow's escalation rules now compare against that score; before, the engine used a constant 0.

Error Handling

400

Invalid Request

Malformed JSON, missing message, missing botId/groupId, or a bad field shape.

401

Authentication Error

Missing or invalid API key.

403

Forbidden

Key lacks the scope, is bot-bound to a different bot, or the org is suspended.

404

Not Found

Bot, group, or resource not found.

429

Rate Limited

Per-key rate limit exceeded — see the Retry-After header.

502 / 503

Engine Unavailable

Runtime engine unreachable, not configured, or temporarily disabled.

Engine-side failures (e.g. a missing BYOK provider key) pass through the engine's status code with its own code / error.

json
// Error response format
{
  "error": "One of `botId` or `groupId` is required.",
  "code": "invalid_request"
}

Multi-Turn Example

Keep a stable sessionId and pass the running transcript in conversationHistory so the bot has context.

Turn 1 - User reports issue

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"botId":"bot_abc123","sessionId":"conv_001","message":"My subscription renewal failed."}'

Turn 2 - User provides order number (include prior turns)

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"botId":"bot_abc123","sessionId":"conv_001","message":"My order is ORD-98765-XYZ","conversationHistory":[{"role":"user","content":"My subscription renewal failed."},{"role":"assistant","content":"I can help — what is your order number?"}]}'

Turn 3 - User wants escalation

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"botId":"bot_abc123","sessionId":"conv_001","message":"I want to talk to a real person."}'

Webhooks

/api/v1/bot-run is synchronous — the turn result is returned in the response. For async notifications (escalations, stage changes, verification failures, tool results), register an Event Connector (Dashboard → Connectors, or via the MCP surface). Rylvo POSTs each subscribed event to your endpoint with the auth method and HMAC signing secret you configure on the connector. Subscribable event types are listed under Connectors.

Rate Limits

POST /api/v1/bot-run is rate-limited per API key — 60 requests/minute by default, overridable per key (Dashboard → API Keys). Bot chat launched from the dashboard uses your own BYOK key and is NOT rate-limited by Rylvo; the platform AI features (Agent Evolution, Self-Improving Prompts, Red Team) also run on your own LLM key and are free — see pricing.

PlanMonthlyBot replies
Free$0Fair use
Lite$40Fair use
Pro$100Fair use · guide ≈ 100K/mo
Team (retired)$400Fair use
EnterpriseCustomSet in your contract

Bot replies are fair use: nothing meters or charges them per plan, and the figures above are guides, not limits.

Rate-limit headers on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After on 429.

Billing & Usage

Rylvo's billing model is simple: a monthly subscription plan covers platform capacity (bots, prompts, guardrails, KB sources, connectors, observability), and every AI feature runs on your own LLM key (BYOK) for free.

Your LLM key (BYOK)

Every AI feature — free

Bot chat, Workspace Architect, Agent Evolution, Self-Improving Prompts, Edge Case Engine, Red Team, broadcasts — all run on your own provider key with zero markup. No AI credits, no markup. Knowledge-base indexing is included up to your plan's allowance.

Prepaid wallet (optional)

Conversation-storage overage

A prepaid USD wallet covers only conversation-storage overage beyond your plan's included pool and wallet-funded retention add-ons. Dodo charges plan subscriptions separately. Top up from $5 at Dashboard → Billing. AI features never draw from it.

Subscription plans

PlanPriceHeadline limitsNotes
Free$01 bot · 7-day trace retentionYour own AI key, zero markup; bot replies are fair use
Lite$403 bots · 14-day trace retentionSolo builder toolkit; no Advanced AI suite or Broadcasts
Pro$10025 bots · 90-day trace retentionAdvanced AI suite + Broadcasts
Team (retired)$40025 bots · 90-day trace retentionPro + you + 2 seats included ($40/extra seat) + dedicated support
EnterpriseCustomNegotiatedCustom DPA, security review, named contact (next business day, not an SLA)

Advanced AI suite (Pro+): Agent Evolution, Self-Improving Prompts, Edge Case Engine, Red Team, Mission Control, Audit Log Export, Custom Policy Blocks. Broadcasts (Pro+): multi-channel outbound campaigns. Lite keeps Advanced Analytics, BYODB, Test Suites, MCP Hub, Webhooks, and Build Intelligence.

Token cost (BYOK — informational)

Bot turns run on your own provider key, so Rylvo does not bill per token — you pay your LLM provider directly. Each turn still records its token cost on the trace for observability and per-bot budget tracking:

text
trace.cost_usd = (input_tokens  × model.price_per_input_token)
               + (output_tokens × model.price_per_output_token)

Response headers on /api/v1/bot-run

HeaderMeaning
X-RateLimit-LimitThe key's per-minute request capacity
X-RateLimit-RemainingRequests left in the current rate-limit window
X-RateLimit-ResetUnix seconds when the window resets
Retry-AfterSeconds to wait before retrying (on 429)
Idempotent-Replaytrue when a stored idempotent response is replayed

Subscription state Dashboard-only

The billing subscription is a singleton document in Firestore. State transitions are driven by provider webhooks plus an hourly lifecycle cron. Only the downgraded status turns paid features off. Legacy plans are migrated to the canonical set at read-time.

Plan gates

  • requireFeature — feature locked or downgraded
  • requireQuota — usage ≥ plan cap
  • requireProviderKey — AI feature needs your own LLM key (BYOK, free)
  • requireStorageEventsBudget — storage pool exhausted
  • Returns PlanAccessError with upgradeTarget

Durable outbox architecture

  • API routes publish to a durable billingOutbox before Pub/Sub
  • Cloud Function billingOutboxWorker is the only account writer
  • Atomic transactions with idempotency + negative clamp
  • Daily billingReconciliation runs ledger integrity checks
  • Port-based design: all billing code depends on abstract interfaces

Per-bot budget cap

Each bot can carry a monthly spend cap. The gate sums the bot's observed (BYOK) cost from the canonical observability store via sum_cost_by_bot and pauses the bot when the cap is exceeded. Set caps in the bot settings.

Top-up & ledger Dashboard-only

The prepaid wallet (conversation-storage overage + retention add-ons) tops up in any amount from $5 to $10,000 at Dashboard → Billing. Every debit and credit is recorded in an immutable transactions ledger you can review and export from the Billing page (per-bot breakdown, model breakdown, date range, CSV).

Bot turns are BYOK — /api/v1/bot-run does not draw from the prepaid wallet or return 402 for balance. The wallet only covers conversation-storage overage and wallet-funded retention add-ons; Dodo charges the plan separately.

Traces & Analytics

Rylvo Traces and Analytics walkthrough

Every decision is traced to a single canonical store — Postgres rylvo-obs (observability_traces → observations → scores). The legacy Firestore chatTraces dual-write was retired. You can replay any past turn, list every run in a session, and pull rollup analytics per company from the dashboard. The engine endpoints below back those views — they are internal (dashboard / MCP), not part of the public REST surface.

Internal endpoints (power the dashboard)

MethodPathDescription
GET/v1/traces/{trace_id}/replayFull replay payload for one trace (prompt stack, guardrails, tool calls, per-stage timings)
GET/v1/sessions/{session_id}/traces?limit=50List every run in a session, newest first
GET/v1/analytics/companies/{company_id}/overview?limit=500&top_n=10Escalation rate, avg latency, and token/cost rollups over the window
GET/v1/analytics/companies/{company_id}/runs?limit=100Recent individual runs for a company

Write paths

  • Engine: make_observability_ingest_hook (live channels)
  • Playground / Architect: adapters → POST /v1/observability/ingest
  • Rich operator context on metadata.turn_context
  • Hierarchical observations: spans, generations, tools, guardrails, retriever, agent

Read paths

  • Dashboard traces: canonical-backed projectors
  • Analytics: analytics-rows Postgres aggregates
  • Billing: sum_cost_by_bot (per-bot cap)
  • Audience / GDPR: canonical customer rows

Typed decisions (TypeSafe Jev)

Some decisions inside a turn only pick a label, so the engine asks them as typed questions of TypeSafe Jev (“System One” by TypeSafe AI) — a decisions-only model that picks one of N options, answers yes/no, or places text on an ordered scale, with a calibrated probability, and never writes text. Every site keeps its previous implementation as an automatic fallback when Jev cannot answer, and a circuit breaker makes that fallback immediate during an outage. Jev runs on Rylvo's own TypeSafe key, not your BYOK provider key; the fallback uses your key as before.

Each Jev request is a generation observation on the trace — named like jev-guardrail-check, jev-workflow-stage, jev-kb-relevance, or jev-flow-intent — with model typesafe/jev-latest, tokens, cost, latency, and every question with its answer and probability. It is priced like any other generation and counts toward the trace total. Question text can quote your customer, so it is stored only when the bot captures inputs. When Jev did not answer, the step records the error and that the previous implementation decided. The risk signal is not a trace step; it appears with the conversation's risk signals in Mission Control. Organizations can opt out of Jev in Settings, and every decision then uses its previous implementation.

Datetime trap: observability started_at columns are TIMESTAMP WITHOUT TIME ZONE. Always use naive UTC at the repo boundary; tz-aware datetimes are rejected by asyncpg.

Replay example

bash
curl https://rylvo.com/api/v1/traces/tr_def456/replay \
  -H "X-API-Key: YOUR_API_KEY"

AnalyticsDashboard-only

Charts on every reply your bots send, read from the canonical observability store (Postgres) — the same replies the Traces page lists. Every part has its own page under Dashboard → Analytics: rooms (each room, its settings and each widget), the widget library, templates, alerts and shared links.

Rooms

A named grid of widgets with its own range and bot.

  • Last hour, 24 hours, 7, 30 or 90 days, all time, or custom days in the workspace time zone
  • The range and bot live in the URL, so a link opens the same view
  • Click a point, bar or slice to open those replies in Traces
  • Test and Production each have their own rooms; releases don't carry them

Widgets

What to count, the measure, how to split it, and the chart.

  • Count replies, tool calls or paid replies
  • Replies, people reached, conversations, cost, tokens, response time, pass / fail / warning / blocked / error rate
  • Over time, or by bot, model, channel, outcome or end user — as a number, line, columns, bars, donut, distribution or table
  • A widget can narrow the room's range or bot, never widen it; number tiles compare with the period before

Library & templates

  • One widget library for Test and Production
  • Adding a library widget to a room places a copy — changing the library doesn't change rooms
  • 10 templates — Overview, Right now, Quality, Speed, Cost, Channels, Customers, Bots compared, Tools, Models — previewed on your live numbers
  • An empty workspace starts with an Overview room on the last 7 days

Alerts

  • Error rate, warning rate, response time (p95), cost per reply or daily spend at or over a line
  • One bot or every bot; Production, Test or both
  • Checked about every 5 minutes; when one fires, every member gets an in-app notification, then it waits its cool-down
  • The form shows what the number is right now

Sharing, events & export

  • A share link is a snapshot — numbers as they were, never reply text; end users appear as “Customer 1, 2…”
  • Links expire (the workspace default, up to its maximum) and can be switched off
  • Events mark a launch or an incident on a room's time charts
  • Export a room's widget numbers, or the replies behind them, as CSV or JSON

How much is read

  • Up to 20,000 of the newest replies in the range, 2,000 per page
  • When a range holds more, the room says which days its numbers cover
  • Replies still running and Workspace Architect turns are left out

Roles and plan

Advanced Analytics is Lite and up; Free workspaces see an upgrade prompt. Viewers see every room and can export; operators create rooms, edit widgets, save to the library and add events; admins delete rooms, manage alerts, make share links and delete library widgets.

Analytics referenceDashboard-only

The shapes and internal endpoints behind Analytics. Rooms are read from Firestore in real time and written only through the engine, which checks the org, plan and role and writes an audit entry. None of these endpoints are public REST.

Internal endpoints (engine)

  • GET /v1/analytics-rooms/{company_id} — list rooms
  • GET /v1/analytics-rooms/{company_id}/{room_id} — one room (404 if missing)
  • POST /v1/analytics-rooms/{company_id} — create (operators+; 201, { room_id, ok })
  • PUT /v1/analytics-rooms/{company_id}/{room_id} — change name, filters or charts (operators+)
  • DELETE /v1/analytics-rooms/{company_id}/{room_id} — delete (admins; idempotent)
  • GET /v1/observability/orgs/{org_id}/analytics-rows — replies for charts, newest first, ≤ 2,000 per call
  • POST /v1/observability/orgs/{org_id}/alerts — create, or update when an id is sent (admins)
  • POST /v1/observability/orgs/{org_id}/alerts/check — an alert's number right now (any member)

Room changes are audited (analytics_room_created / updated / deleted), and so are share links (analytics.share_created / analytics.share_revoked).

Alerts

  • metric: error_rate, warning_rate, p95_latency_ms, avg_cost_usd, daily_spend
  • environment: production, test, or null for both; botId null = every bot
  • Rates are stored as fractions (0.05 = 5%); the form works in percent
  • Fires at or over the threshold with at least minTraces replies, then waits cooldownMinutes

Roles

  • analytics.view — viewer
  • analytics.edit, analytics.annotate — operator
  • analytics.manage, analytics.share — admin
  • Library widgets: operators write, admins delete; events and share links are server-only

Firestore shape

text
{envRoot}/analyticsRooms/{roomId}          (Test and Production each have their own)
  |- name (1-200), createdBy, createdAt, updatedAt
  |- filters: { botId, env, dateRange }     (the room's own range and bot)
  +- charts: [{ id, type, position, title?, width?, height?, color?,
                customWidget?: WidgetConfig }]

WidgetConfig:
  view: traces (replies) | tool_calls | cost (paid replies)
  metric: count | total_cost | p95_latency | error_rate | ... (20)
  breakdown: none | time | bot | model | status | user | channel | ...
  chartType: big_number | line | bar_v | bar_h | pie | histogram | pivot
  filters: [{ id, column, op, value }], dateRange (narrows only), topN? (10)

{orgRoot}/analyticsWidgets/{widgetId}  library (Test + Production)
{orgRoot}/analyticsAnnotations/{id}    events (server-written)
analyticsShares/{token}                share links + snapshot (server-only)

SDKs & Libraries

Python

python
import requests

resp = requests.post(
    "https://rylvo.com/api/v1/bot-run",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "botId": "bot_abc123def456",
        "sessionId": "sess_001",
        "message": "I can't access my account.",
    },
)

data = resp.json()
print(data["response"])
print(f"run {data['runId']} · {data['tokens']['total']} tokens")

JavaScript / Node.js

javascript
const res = await fetch("https://rylvo.com/api/v1/bot-run", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_API_KEY",
  },
  body: JSON.stringify({
    botId: "bot_abc123def456",
    sessionId: "sess_001",
    message: "I can't access my account.",
  }),
});

const data = await res.json();
console.log(data.response);
console.log(data.runId, data.tokens.total, "tokens");

PromptsDashboard-only

Rylvo Prompts walkthrough

Prompts are the instructions your agents follow at runtime. Create them from the Prompts dashboard, assign them to bots, and optionally enable self-improving optimization.

Key Concepts

Agent Categories

9 categories: System, Response Composer, Stage Classifier, Action Selector, Escalation Classifier, Session Summarizer, Verifier, Retrieval, Custom

Placeholders

Use {{key}} syntax for dynamic values resolved at runtime (company name, tone, language, etc.)

Self-Improving

Toggle self-improving prompts to let the system generate, evaluate, and promote better prompt variants

Bot Assignment

Assign prompts to specific bots, or leave shared across all bots

Creating a Prompt

1

Choose an agent category

Select what type of agent this prompt controls (e.g., Response Composer, Verifier).

2

Write your prompt content

Use {{placeholder_key}} syntax for dynamic values. Templates are provided for each category.

3

Add placeholders

Define keys, labels, default values, and whether they're required. Use the template library for common ones.

4

Assign to a bot (optional)

Link the prompt to a specific bot, or leave it shared. Tag requests with prompt_id in the API.

5

Enable self-improving (optional)

Toggle auto-optimization in the prompt detail panel. Choose a strategy, metric, and frequency.

Placeholder Syntax

Placeholders use double-brace syntax: {{key}}. At runtime, they are resolved with values from the API request or defaults.

text
You are a support agent for {{company_name}}.

Greet the user as {{user_name}} in a {{tone}} tone.
Respond in {{language}}. Keep replies under {{max_length}} tokens.

{{context}}

Built-in Placeholder Templates

KeyDefaultDescription
{{company_name}}Acme CorpThe customer's company name
{{user_name}}JohnThe end user's display name
{{product_name}}Widget ProThe product being discussed
{{industry}}TechnologyThe customer's industry vertical
{{tone}}professionalResponse tone (formal, casual, empathetic)
{{language}}EnglishResponse language
{{max_length}}256Maximum response length in tokens
{{context}}-Additional context for the prompt

Version History

Every edit to a prompt creates a new version. Versions are tracked as manual or auto_optimized. You can view, compare, and promote any version to active from the prompt detail panel.

Prompt inheritance

Fork a prompt by setting a parentPromptId. The child inherits the base template and can add its own overrides while keeping the lineage tracked in version history. This is useful for per-bot or per-group variations of a shared prompt.

Dependency graph

The Prompts dashboard has an interactive SVG graph showing how prompts, bots, and agent groups relate to each other.

Graph layout

  • Bots on the left → Prompts in the center → Agent groups on the right
  • Edges show bot prompt assignments via prompt.botId
  • Org-level prompts (botId = null) connect to an org virtual node
  • Parent → child inheritance drawn within the prompt column
  • Group-shared prompts connect to their agent group

Interactions

  • Click a prompt → open prompt detail
  • Click a bot → open bot page
  • Hover any node → tooltip with full name and metadata
  • Scroll/pinch to zoom, drag to pan
  • Zoom/fit buttons for quick navigation

Prompt usage analytics

The prompt detail panel shows real-time usage metrics from the canonical trace store: trace volume, cost, latency, and A/B run history. Use this to decide which version is performing best before promoting it.

Resource VersioningDashboard-only

Every runtime-affecting resource is versioned - not just prompts. Prompts, guardrails, connectors, evolution rules, and KB blueprints all track an immutable version history plus the source of each edit. You can diff, preview, promote, and rollback from the dashboard.

Rules and scoring too. The Edge Case detection rules (with the AI review's criteria), test scoring and the release gate each keep a version per save — who saved it, when, the note, how many things changed — listed under History on its page, on every plan. On Enterprise, Restore puts an earlier version back: it's saved as a new version (the history keeps both), checked like any save — a plan limit or a rule that no longer exists stops it and says why — and written to the audit log.

As code. Each of the three is one JSON document: the field detection, scoring or criteria of its GET, which its PUT takes back with base_version (a stale one is refused) and an optional change_note — so a pipeline can keep them in git. On Enterprise, Export downloads the saved version as that JSON, and Import puts a file — an export, or a saved API response — on the page as unsaved changes to check and save. Settings the page doesn't have are left out and named. Over MCP (Build Intelligence), the same three are edge_cases.get/set_detection_rules, tests.get/set_scoring and tests.get/set_release_gate, with workspace.restore_config_version to put one back — saving is admins and owners, and the history names the person (“Alex over MCP”).

Edit sources

human

Manual edit by a teammate from the dashboard

architect

Applied via Workspace Architect (conversational setup / edit mode)

auto_generated

Produced by the Self-Improving optimizer or Agent Evolution rule generator

fix_proposal

Accepted from an Edge Case Engine fix proposal

system

Internal migration or default seed

red_team

Created in response to a red-team finding

What's versioned

Bots
Prompts
Guardrails
Connectors
Evolution Rules
KB Blueprints

What you can do from the dashboard

  • View the full edit history for any resource (who, what, when, why)
  • Diff any two versions side by side
  • Preview a version before promoting it to active
  • Promote any past version back to active in one click (rollback)
  • Filter history by source - manual, architect, auto-generated, etc.

Every runtime decision records which version was active when it ran, so you can correlate performance changes with specific edits - not just which resource was used, but which version of it.

Self-Improving PromptsDashboard-only

Rylvo Self-Improving Prompts walkthrough

Rylvo implements research-backed automatic prompt optimization. When enabled, the system continuously generates, evaluates, and selects better prompt variants using real performance data.

How It Works

1. Collect

The system gathers performance metrics from real API requests that use this prompt - success rate, verification pass rate, user satisfaction signals, and latency.

2. Generate

An optimizer LLM analyzes scored prompt history and proposes N improved variants. Each variant preserves all placeholders and the core intent, but tries a different improvement strategy.

3. Evaluate

Each variant is scored against your chosen metric. Scoring uses structural analysis, constraint quality, and historical performance patterns.

4. Select

The best-performing variant is identified. It must beat the current prompt by at least the improvement threshold (default 2%) to qualify.

5. Promote or Suggest

If auto-promote is on, the winner automatically becomes the active prompt. Otherwise, it appears in version history for manual review.

Optimization Strategies

Four research-backed strategies are available. Each uses a different approach to prompt improvement.

OPRO

Yang et al., 2023 - Google DeepMind

Optimization by PROmpting. Feeds scored (prompt, performance) history to an LLM which proposes improved variants. Core insight: LLMs can act as optimizers when given examples with scores.

Best when you have version history with diverse scores.

APE

Zhou et al., 2023 - University of Toronto

Automatic Prompt Engineer. Generates diverse candidates using multiple improvement strategies (explicit instructions, examples, restructuring, guardrails, simplification), evaluates each, selects the best.

Best for new prompts with little history.

DSPy Bootstrap

Khattab et al., 2023 - Stanford

Compiles the best-performing patterns from history into optimized few-shot demonstrations. Extracts what works and bakes it directly into the prompt as examples.

Best when high-scoring versions exist to learn from.

PromptBreeder

Fernando et al., 2023 - Google DeepMind

Self-referential evolution. Applies mutation-prompts (make concise, add constraints, restructure, add reasoning, add role-playing) to evolve the task-prompt. The mutations themselves can evolve.

Best for creative exploration of prompt space.

Configuration Options

OptionDefaultDescription
StrategyOPROWhich optimization algorithm to use
Evaluation MetricSuccess Ratesuccess_rate | verification_pass | user_satisfaction | latency
Run FrequencyDailyhourly | daily | weekly | manual
Auto-PromoteOffAutomatically promote winning variants to active
Improvement Threshold2%Minimum improvement required to qualify as a winner
Max Variants Per Run5Number of candidate variants generated per optimization run
Min Sample Size50Minimum API requests before first optimization run

Enabling Self-Improvement

1

Open the prompt detail panel

Click on any prompt from the Prompts page to view its details.

2

Expand 'Self-Improving Prompts'

Click the panel header to expand the optimization settings.

3

Toggle 'Enable Auto-Improvement'

This activates the optimization loop for this prompt.

4

Choose your strategy

Select OPRO, APE, DSPy Bootstrap, or PromptBreeder depending on your needs.

5

Set your metric and frequency

Choose what to optimize for and how often to run.

6

Decide on auto-promote

Turn on to automatically deploy winning variants, or leave off for manual review.

Prompt API Usage

Prompts are assigned to a bot in the dashboard (or via the MCP surface), versioned, and resolved automatically when the bot runs. /api/v1/bot-run does not accept a prompt_id orprompt_overrides — to change which prompt a bot uses, update its prompt assignment.

How prompts resolve at runtime

1

Resolve

The bot's assigned prompts load their active versions.

2

Substitute

All {{placeholder}} values are filled from bot/org defaults.

3

Inject

The resolved text becomes part of the system prompt for the turn.

4

Track

The trace records which prompt versions were active (drives analytics + Self-Improving Prompts).

Change or A/B a prompt

  • Update a bot's prompt assignment in Dashboard → Bots (or the Prompts page).
  • Use Self-Improving Prompts to A/B variants and auto-promote winners.
  • Every turn's trace records the resolved prompt version for analytics and optimization.

Edge Case Engine Dashboard-only

Automatically detect, classify, and fix bot failure modes

Rylvo Edge Case Engine walkthrough

The Edge Case Engine scans your bot traces and files what they actually recorded into 15 categories, with the traces as evidence. It works in four stages: Observe → Detect → Classify → Triage, and a resolved case reopens by itself if the problem comes back. Manage from Dashboard → Edge Cases. Pro+ plan feature.

3 detection mechanisms

  1. Trace scan — POST /v1/edge-cases/{org}/scan files what each trace recorded (failure verdicts and knowledge gaps), one case per signal per bot, with the traces as evidence. 10 scans/min per org
  2. AI review — optional: a model you choose reads the real conversations and may only report defects visible in the reply, citing the trace. Runs free on your own LLM key (BYOK)
  3. Red Team adversarial testing — 13 built-in strategies with static + LLM-generated inputs, runs synchronously on Cloud Run with per-result checkpointing

12 edge case categories

  • Hallucination — response not grounded in KB evidence
  • Policy Violation — broke company/workflow policy
  • Missed Escalation — should have escalated but didn't
  • Wrong Tool — selected incorrect tool
  • Missing Tool Call — should have called tool but skipped
  • Wrong Stage — misidentified workflow stage
  • Stale Memory — used outdated session facts
  • Unknown Intent — intent not covered by workflow
  • Tone Violation — doesn't match brand guidelines
  • PII Leak — exposed sensitive personal info
  • Loop Detected — bot stuck in repetitive pattern
  • Custom — user-defined category

Status lifecycle & severity

5 severity levels (critical=5, high=4, medium=3, low=2, info=1) sorted by weight.

Status flow:

open → acknowledged → fix_proposed → fix_applied → resolved

Alternative: dismissed (false positive) or deleted (permanent removal).

Fixes

Suggest fixes on an edge case (or on Fixes) gives Rylvo's fixes for that kind of problem, plus up to three the AI writes for the exact conversation on your model key. Each is one of four kinds:

  • Guardrail — added in Test in one click (or edited first); a release puts it live
  • Prompt — text to add to the bot's prompt: copy it, add it, mark it done
  • Knowledge — an answer to add to a knowledge base, then mark it done
  • Setting — steps to check, with a link to the place

Rylvo never edits a prompt or a knowledge base for you. Fixes can also be posted through the API (POST /v1/red-team/{org}/fix-proposals).

Lifecycle: to do → done / dismissed

13 Red Team strategies

  • policy_probe, hallucination_bait, tool_confusion, escalation_boundary
  • injection_attempt, context_overflow, mcp_abuse, pii_extraction
  • multi_turn_coercion, jailbreak_dan, function_leak, billing_bypass, kb_poisoning

Tuning: 240s run budget, 45s per-turn timeout, 6 concurrent, 25 tests/strategy max. Guardrails fire as in production; billing/governance skipped (synthetic runs free).

Deduplication & RBAC

Deterministic ID from SHA256(botId::category::title). Duplicate scans increment occurrenceCount.

  • edgeCases.view — viewer+
  • edgeCases.manage — admin+
  • guardrails.view — viewer+
  • guardrails.manage — admin+
  • redTeam.view — viewer+
  • redTeam.run — admin+

4 dashboard tabs

Edge Cases

Filter by status/category/severity/bot, search, create/edit/resolve/dismiss/delete, assign

Red Team

Create runs, select strategies, configure, execute synchronously, view results, create edge cases from failures

Guardrails

View/create/edit/toggle/delete, conditions, actions, priorities, trigger stats, version history

Fix Proposals

View/create/apply/reject/revert with regression test linking

Firestore shape

text
{envRoot}/edgeCases/{edgeCaseId}
  |- title, description, category, severity, status
  |- source: trace_mining / red_team / manual / guardrail_trigger
  |- botId, botName, traceIds[], occurrenceCount
  |- sampleInput, sampleOutput, expectedBehavior
  |- fixProposalId, guardrailId, tags, assignedTo
  +- firstSeenAt, lastSeenAt, resolvedBy, resolvedAt

{envRoot}/fixProposals/{proposalId}
  |- edgeCaseId, fixType, title, description
  |- impact, confidence, status
  |- details: { guardrailConfig?, promptChanges?, thresholdChanges?, customSteps? }
  |- regressionTestIds[]
  +- appliedBy, appliedAt, createdBy, createdAt

{envRoot}/redTeamRuns/{runId}
  |- name, botId, strategies[], status
  |- config: { testsPerStrategy, maxConcurrent, timeoutMs, ... }
  |- results: { totalTests, passedTests, failedTests, edgeCasesFound }
  +- edgeCaseIds[], startedAt, completedAt

{envRoot}/redTeamRuns/{runId}/results/{resultId}
  |- strategy, testInput, botResponse, passed
  |- failureCategory, failureReason, severity, edgeCaseId
  |- guardrailResults[], toolCalls[], traceId, latencyMs
  +- model, tokens, createdAt

Guardrails Dashboard-only

Programmable safety rules that run on every bot response

Rylvo Guardrails walkthrough

Guardrails are runtime rules that check bot inputs/outputs and take action when conditions are met. They can be created manually or auto-suggested by the Edge Case Engine when it detects failure patterns.

Guardrail Types

  • Input Filter - Validates user input before processing
  • Output Filter - Validates bot response before delivery
  • Fact Check - Verifies claims against knowledge base
  • Policy Check - Ensures compliance with company policies
  • PII Detection - Detects and redacts sensitive data
  • Tone Check - Validates tone matches brand
  • Loop Breaker - Breaks repetitive conversation patterns
  • Escalation Override - Forces escalation on conditions
  • Tool Gate - Controls tool access in specific contexts

Actions

  • Block - Prevent response, return fallback message
  • Warn - Flag for review but allow response through
  • Rewrite - Auto-modify response to comply
  • Escalate - Trigger human handoff immediately
  • Log - Record for monitoring, no action taken

Each guardrail has conditions (field + operator + value) and a fallback message shown when triggered.

Conditions & evaluation

A guardrail condition is built from a trace/runtime field, an operator, and a value. Examples:verification.status == fail,output.pii_detected == true, orloop.count > 3. Guardrails are evaluated in priority order; block and escalate actions short-circuit the rest.

Custom evaluation (custom_eval) conditions take a rule in plain language. Every custom rule that checks the same text is judged in one TypeSafe Jev request rather than one LLM call per rule, and the reason carries the probability, e.g. Violates "Never promise a refund" (p=0.94). If Jev cannot answer, the LLM judge runs on your key; the fail-closed policy is unchanged — if no judge can answer, the rule counts as violated.

Lifecycle & audit

Versioned

Every edit is a new version tagged manual, fix_proposal, red_team, system, or auto_generated

Experiment-gated

Risky changes can be deployed behind a flag before full promotion

Trace audit

Every guardrail decision is recorded in the trace observations

Guardrail Audit

Scheduled automation task reviews effectiveness and suggests tuning

Red Team Testing Dashboard-only

Proactively discover vulnerabilities with adversarial tests

Rylvo Red Team Testing walkthrough

Red Team runs generate adversarial inputs against your bot to find failures before users do. Select a bot, choose attack strategies, and the engine generates test cases automatically. Results are converted into edge cases with severity levels.

13 built-in attack strategies

  • Policy Probe - Tests policy boundary conditions with edge-case inputs
  • Hallucination Bait - Questions with no KB answer to test grounding
  • Tool Confusion / MCP Abuse - Inputs that trigger wrong or unsafe tool selection
  • Escalation Boundary - Tests the edge of escalation thresholds
  • Injection Attempt / Jailbreak (DAN) - Prompt injection and jailbreak attempts
  • Context Overflow - Long conversations that stress memory limits
  • PII Extraction / Function Leak - Attempts to surface private data or system internals
  • Multi-Turn Coercion, Billing Bypass, KB Poisoning

Automation & Reports Dashboard-only

Schedule scans, connect channels, and generate automated reports

Rylvo Automation and Reports walkthrough

The Automation page at /dashboard/automation lets you set up recurring jobs, notification channels, and automated analytics reports. Everything runs on your chosen schedule — no manual intervention needed after setup. Every part has its own page: Overview, Tasks (each with its runs and settings), Reports (each with its history and settings), Generated reports, and Channels. You edit automations in Test; the released copy runs and sends in Production, while Test runs are saved on each task page and never sent.

10 scheduled task templates

Cron jobs that run automatically via Cloud Function (every 5 min):

  • trace_scan — auto-scan traces for edge cases
  • red_team_run — periodic adversarial testing
  • guardrail_audit — review guardrail effectiveness
  • complaint_watch — monitor complaint-risk conversations
  • dev_review — review failed runs & patterns
  • conversation_quality — sample conversations for quality review
  • cost_usage_watch — review recorded token spend & cost
  • kb_gap_analysis — find knowledge gaps in trace outcomes
  • connector_health — audit connector/MCP health
  • test_suite_run — run a test suite when the engine integration is configured

9 cadences: hourly → monthly. Rich schedule config (interval/weekly/monthly with IANA timezone). Max 25 tasks/invocation, 3 concurrent/org, 90s timeout. Selected workers can add optional BYOK analysis; reports are configured separately.

5 notification channels

Where alerts and reports get sent. Delivery via durable outbox with exponential backoff retry:

  • Slack — webhook integration
  • Email — direct to team inboxes
  • Discord — webhook integration
  • Microsoft Teams — webhook integration
  • Custom Webhook — any HTTPS endpoint

Events: edge_case.created, red_team.completed, report.generated, etc. Scope: global or linked to specific bots.

13 report sections

  • executive_summary, edge_cases_overview, edge_cases_by_category/severity
  • guardrail_performance, red_team_results, fix_proposals_summary
  • improvements_timeline, bot_comparison, complaints_and_escalations
  • developer_followups, automation_coverage, recommendations (AI)

13 data sources, 5 working output formats (html_email, slack_blocks, teams_card, discord_embed, markdown). 5 recipient filters (all_members, admins_only, owners_only, operators_only, custom). Optional LLM formatting with custom model, system prompt, temperature, max tokens. The reserved PDF option does not currently generate a PDF file.

RBAC & plan limits

  • automation.view — viewer+
  • automation.generate_report — operator+
  • automation.manage_tasks — admin+
  • automation.manage_channels — admin+
  • automation.manage_reports — admin+

Free: 0 / Lite: 10ch, 10tasks, 2configs, 25reports/mo / Pro: 25, 25, 5, 50 / Team: 25, 25, 5, 100 / Enterprise: unlimited.

Firestore shape

text
{envRoot}/scheduledTasks/{taskId}
  |- name, type, cadence, enabled, botId
  |- config: { traceLimit?, redTeamStrategies?, aiConfig?, scheduleConfig? }
  |- notificationChannelIds[], lastRunAt, lastRunStatus, nextRunAt
  +- runCount, failCount

{envRoot}/notificationChannels/{channelId}
  |- name, type, enabled, config, events[], botIds[], scope
  +- lastUsedAt, lastStatus

{envRoot}/reportConfigs/{configId}
  |- name, cadence, sections[], dataSources[], filters
  |- recipientFilter, customRecipientEmails[], notificationChannelIds[]
  |- aiConfig?, outputFormats[], scheduleConfig?
  +- lastGeneratedAt, nextGenerateAt

{envRoot}/reports/{reportId}
  |- configId, status, sections[], data, distributedTo[]
  |- llmFormattedContent?, pdfUrl?, tokenUsage?
  +- periodStart, periodEnd, generationDurationMs

{envRoot}/scheduledTaskRuns/{runId}
  |- taskId, taskType, status, summary, details
  +- startedAt, completedAt

Agent Evolution Dashboard-only

Closed-loop system that learns from real traffic and safely rewrites the agent

Rylvo Agent Evolution walkthrough

While the Edge Case Engine catches individual failures, Agent Evolution looks at patterns across runs and updates the agent itself. It runs a six-phase loop - observe, detect, infer, generate, inject, verify - and writes new rules, prompt variants, or guardrail tweaks into versioned resources. Every change is reversible.

Phases

  1. Observe - sample recent traces for the bot
  2. Detect - cluster failures & near-misses
  3. Infer - reviewer LLM names the pattern + root cause
  4. Generate - draft a rule, prompt edit, or guardrail
  5. Inject - attach to the bot behind an experiment flag
  6. Verify - measure effect; auto-promote, keep, or roll back

What gets written

  • Evolution rules (injected into the system prompt conditionally)
  • Prompt version updates (marked auto_generated)
  • Guardrail tweaks (threshold, condition, fallback text)
  • Reviewer insights (narrative explanations for the team)
  • Effectiveness scores per rule (controls auto-promote)

Dashboard tabs

Overview

Bot health score, recent improvements, active experiments, and run summary

Insights

Detected patterns, trend cards, and recommended actions

Failures

Failing traces grouped by root cause; promote to Edge Cases

Rulebook

Active and pending evolution rules with experiment flags and rollback

Improve

Run a manual improvement cycle on selected traces or prompts

Dialectic model

Reviewer model settings for critique and refinement

Settings

Enable/disable modules, trace filters, cadence, notifications, and what each reviewer looks for

Run logs

History of every evolution run with status, cost, and artifacts

What each reviewer looks for

Four AI reviewers read a bot's latest replies — the pattern, speed, answer-quality and input reviewers — and each works from a brief. In Agent Evolution → Settings → Reviews you can change a reviewer's brief for the bot: the things it looks for, one per line, and for two of them a number — how many replies a pattern needs before it is reported (2 to start), and how long a step can take before it counts as slow (2,000 ms to start). How a reviewer reports what it finds stays fixed: your brief is handed to it beside the replies, never as its instructions. A pattern that cites fewer replies than its brief asks for is set aside, and the speed reviewer sees each step's time. Only what you change is saved, so Use the shipped brief brings back Rylvo's own. Every review — started from the dashboard, after a run of failures, or after a chat on the bot's page — runs on Rylvo's servers, on your own LLM key.

Self-Improving Prompts

A focused Agent Evolution workflow for prompts. It analyzes prompt-version performance from live traces, generates proposed revisions with explanations, evaluates them against held-out traces, and publishes winning versions as new prompt versions (not live until you promote them). It runs on your own LLM key (BYOK) and is free.

Every proposed change is gated behind an experiment flag with measurable success criteria. Anything that doesn't move the needle is rolled back automatically; anything that does becomes a permanent versioned change you can inspect, fork, or revert.

Agent Skills Dashboard-only

Reusable capability packages that extend your bots

Rylvo Agent Skills walkthrough

Skills are reusable, versioned instructions a bot follows only when a customer's message calls for them. A skill fires when the message contains one of its words or one of its phrases as written (or on every reply, when it has neither), and its instructions are added to the bot's system prompt for that reply. Skills are part of Pro and higher, and run for a bot once its Skills switch is on. Write your own, add one from the library, import them, or review the ones suggested from what Agent Evolution learned. Manage from Dashboard → Agent Skills.

Skills in production

  • Reusable instructions — guide the agent when a skill activates
  • Words and phrases — decide which messages they apply to
  • Bot or org scope — control where a skill applies
  • Versions and telemetry — review changes and live usage

Trigger matching (deterministic, no LLM)

  • A word matches anywhere in the message, even inside a longer word
  • A phrase matches as written — its quoted part, or each part between commas (5+ characters)
  • One word or one phrase clears the default minimum (0.25); at most 8 skills per reply
  • Common words (“can”, “about”, “they” …) are never triggers — they're dropped and ignored
  • Optional matching by meaning: embedding similarity at weight 0.3
  • No words and no phrases: added to every reply, first

6 skill sources

  • manual — operator creates via dashboard
  • generated_from_rules — auto-promoted from rule clusters (opt-in)
  • imported — imported from another bot/org
  • architect — created by Workspace Architect
  • reviewer_suggested — suggested by reviewer engine
  • prebuilt — installed from the Skill Library catalog

Runtime injection & safety

  • Matched skills added to BotPromptInputs.skill_instructions
  • Merged into system prompt under "## ACTIVATED SKILLS"
  • Wording that tries to override the bot's instructions is refused on save
  • Only the instructions (and tool hints) are sent — never the name or description
  • Usage recorded post-turn via SkillUsageEventDoc
  • Confidence: score × 0.9 (success) or × 0.2 (failure) minus guardrail penalty

Scoping & lifecycle

  • Bot-scoped — botId = specific bot, only that bot loads it
  • Every bot — botId = null, every bot loads it
  • Runtime loads bot-scoped first (priority), then org-level
  • Status: draft → active → disabled → archived
  • Only a skill that's on is used; Production uses the released copies

Versioning, rollback & auto-promotion

  • Every change creates a SkillVersionDoc (7 change types)
  • Putting a version back restores its text and triggers; on/off stays as it is; counters restart
  • Auto-rollback switches off a failing skill and holds it in Test and Production until an admin releases it
  • Suggestions from learned rules: opt-in per bot, after every completed review (cluster ≥ 3, configurable 2–10), always as drafts
  • Effectiveness: usageCount, successCount, avgConfidence, lastUsedAt

RBAC & plan limits

  • evolution.edit_config — admin+ (manage skills)
  • Viewers can see but not create/edit/delete/status/rollback

Pro, Team and Enterprise: unlimited skills · Free and Lite: not included (existing skills are kept)

Dashboard pages

  • All skills — who uses each one, what fires it, how it's doing
  • Each skill — overview, instructions, triggers, versions, settings
  • Try a message — which skills a message fires, and what the model receives
  • Library — 50 ready-made skills across 15 categories
  • Settings — each bot's Skills switch, matching, suggestions, auto-rollback

Switch skills on for a bot in Skills → Settings.

Skill Library (Prebuilt Catalog)

A curated catalog of 50 ready-made skills across 15 categories covering both single-agent and multi-agent bot templates (Customer Support, Sales, Compliance, Data Collection, Conversation Design, Knowledge & Escalation, E-commerce, Healthcare, HR & Internal, Finance & Insurance, IT & Technical, Multi-Agent, Content & Marketing, Real Estate & Education, Utilities). Browse in the Skills → Library and add a copy to one bot or to every bot.

Fork model

  • Installing creates a copy you own (source: "prebuilt")
  • No auto-updates — edit freely after install
  • As a draft or turned on — you choose
  • For one bot or for every bot — you choose
  • Fires on the catalog's phrases (short ones like “vip” also become words)

Guardrails

  • Each library skill can be added once per bot and once for every bot
  • Add several at once — to one bot or every bot, skipping ones already there
  • Installed skills are normal skills — edit, clone, delete, rollback

Firestore shape

text
{envRoot}/agentSkills/{skillId}
  |- skillId, orgId, botId (null = org-level), botName
  |- name, slug, description, skillType, source, status
  |- systemInstructions (max 8000), triggerConditions[], triggerKeywords[]
  |- requiredTools[], toolChain[], generatedFromRuleIds[]
  |- version, previousVersionId
  |- usageCount, successCount, failureCount, avgConfidence, lastUsedAt
  +- tags, metadata, createdBy, createdAt, updatedAt

{envRoot}/agentSkills/{skillId}/versions/{versionId}
  |- version, changeType, previousInstructions, newInstructions
  |- previousStatus, newStatus, changeReason
  |- previous/newTriggerConditions, previous/newTriggerKeywords
  +- usageCountAtChange, changedBy, createdAt

organizations/{org}/skillSafetyHolds/{skillId}
  +- active, reason, failureRate, usageCount, createdAt, releasedAt?

{envRoot}/agentSkills/{skillId}/usageEvents/{eventId}
  |- eventId (turnId:skillId), turnId, outcome, confidence, score
  +- createdAt, aggregated

Mission Control Dashboard-only

Live human oversight for every production conversation

Rylvo Mission Control walkthrough

Mission Control is the live operations surface at /dashboard/mission-control. Operators see every active conversation in real time, can intervene at any level, and the system keeps an immutable audit trail that satisfies EU AI Act Article 14 (human oversight) requirements. Real-time via Firestore onSnapshot listeners (no WebSocket/SSE).

6 intervention types

  • Whisper — inject system instruction (customer doesn't see), injected into prompt via fetch_active_whispers
  • Takeover — operator becomes respondent, bot paused
  • Pause — freeze the bot, customer sees wait message
  • Kill — terminate session immediately, send fallback (irreversible)
  • Resume — resume a paused/taken-over conversation
  • Flag — flag for review without interrupting

Blocking interventions (pause/takeover/kill) trigger the Python engine pre-gate (evaluate_mission_control_gate) which blocks the bot from responding. Rationale required for pause/takeover/kill.

Approvals & SLAs

3 urgency tiers with timeouts:

  • immediate — 15s timeout (low-risk informational)
  • standard — 120s (data access, PII-related)
  • extended — 900s (financial, legal, compliance)

Lifecycle: pending → approved / denied / expired / auto_denied. ExpiresAt computed server-side. Decisions attached to the trace.

11 alert triggers

Per-conversation (inline after each bot turn):

  • risk_score_above, guardrail_blocked, guardrail_triggered_n_times
  • conversation_duration_above, no_operator_monitoring, approval_queue_size_above
  • pattern_detected (regex), budget_threshold_crossed

Server-tick (scheduled job every minute):

  • bot_turn_failed_n_in_window, byok_key_unhealthy, wallet_balance_below

Severity: critical/high/medium/low. Cooldown (default 300s). Suppression: snooze + quiet hours (IANA timezone). Alert instances idempotent via groupKey (ruleId:conversationId).

Operator presence

  • 4 states: online / away / busy / offline
  • 30s heartbeat, stale after 3 min → auto-offline
  • Shift tracking: shiftStart / shiftEnd
  • Capacity: maxConcurrentMonitors (default 5)
  • Stats: totalInterventions, totalApprovals, avgResponseTimeMs
  • Cross-tab localStorage leader election for sweep

Risk scoring & compliance

  • Risk score 0–100, 5 levels: critical / high / medium / low / minimal
  • Fixed weights: guardrail 30, pattern 25, sentiment 20 (English keyword lists), topic 15, duration 10
  • Plus, on live channel turns, a custom signal (weight 10) from TypeSafe Jev: frustration, urgency and cancellation-threat probabilities over the last 5 user turns, not limited to English. It can add risk, never lower it
  • Risk signals: source, name, score, details, timestamp
  • Compliance modes: standard / eu_ai_act / soc2 / hipaa / custom
  • Append-only oversight audit log (every operator action)
  • Compliance export (CSV/JSON) with signed URLs
  • Retention: plan-based (7d/30d/90d/unlimited), overridable

RBAC

  • missionControl.view — viewer+
  • missionControl.intervene — operator+
  • missionControl.resolve_approval — operator+
  • missionControl.ack_alert — operator+
  • missionControl.manage_rules — admin+
  • missionControl.edit_settings — admin+
  • missionControl.delete_conversation — admin+

7 dashboard tabs

Live Feed

Real-time conversation list (max 100), detail panel with transcript + interventions + risk signals, quick actions, assign operator

Alerts

Active (bulk ack, snooze, resolve), snoozed, history, rules CRUD with evidence chips

Channels

Per-channel connectivity health, error rates, retry queue

Analytics

KPIs, charts by type/severity/status, operator leaderboard (7d/30d/90d)

Audit Log

Append-only event log, filters, compliance export (CSV/JSON)

Operators

Team roster with status, role, shift indicator, stats

Settings

Enable/disable, approval urgency, auto-monitor, compliance mode, retention, messages

API endpoints

  • POST /api/mission-control/intervene — create intervention (operator+)
  • POST /api/mission-control/revert — revert intervention (operator+)
  • POST /api/mission-control/alert-rules — create/update alert rule (admin+)
  • POST /api/mission-control/dispatch — push operator message to channel (operator+)
  • POST /api/mission-control/delete-conversation — hard-delete + cascade (admin+)

Firestore shape

text
{envRoot}/liveConversations/{conversationId}
  |- botId, conversationId, status, riskScore, riskLevel
  |- riskSignals[], messageCount, durationMs, lastMessagePreview
  |- guardrailTriggeredCount, assignedOperatorId, monitoringOperatorIds[]
  |- channel, isProduction, tags, flaggedReason
  +- startedAt, lastMessageAt, endedAt

{envRoot}/operators/{uid}
  |- status: online/away/busy/offline, maxConcurrentMonitors
  |- totalInterventions, totalApprovals, avgResponseTimeMs
  +- shiftStart, shiftEnd, lastActiveAt

{envRoot}/interventions/{interventionId}
  |- type, operatorId, rationale, payload, outcome
  |- riskScoreAtTime, messageCountAtTime, idempotencyKey
  +- createdAt, resolvedAt

{envRoot}/approvalRequests/{requestId}
  |- actionType, urgency, timeoutSeconds, status
  |- riskScoreAtTime, riskSignals[], conversationContext
  +- requestedAt, expiresAt, resolvedBy, resolvedAt

{envRoot}/alertRules/{ruleId}
  |- trigger, severity, config, botId, notificationChannelIds[]
  |- cooldownSeconds, snoozedUntil, quietHours
  +- lastTriggeredAt, triggerCount

{envRoot}/alertInstances/{alertId}
  |- ruleId, status, groupKey, fireCount, evidence[]
  +- firstFiredAt, lastFiredAt, acknowledgedAt, resolvedAt

{envRoot}/oversightAudit/{entryId}
  |- eventType, operatorId, conversationId, botId
  +- details, riskScoreAtTime, timestamp

{envRoot}/missionControl/config
  |- enabled, defaultApprovalUrgency, autoMonitorHighRisk
  |- complianceMode, auditRetentionDays, maxOperatorsPerConversation
  +- pauseMessage, takeoverMessage

Flows

Guided/strict conversation graphs that steer a bot through a sequence of steps — each step carries a goal (injected into the system prompt), data to collect, an allowed-tools scope, and transitions the engine advances as the conversation progresses. Flows are org-scoped, attached to a bot via bot.flowId or an agent group via group.flowId, evaluated per turn by the engine, and not part of the /api/v1/bot-run request or response. On every plan — Free includes 1 flow, Lite 10, Pro and up unlimited.

Flow document (FlowDoc)

  • name, description
  • mode: guided | strict
  • start_step_id: entry step (must be defined)
  • steps: FlowStep[]
  • enabled: boolean

Step structure (FlowStep)

  • id (unique), name
  • goal: injected into the system prompt for this step
  • collect: information to ask for before advancing — the flow keeps what the bot's data collection can't save
  • allowed_tools: string[] | "all" | "none" — enforced in strict mode, guidance in guided
  • allowed_agent_id: sub-agent for this step (groups)
  • terminal: ends the flow

Transition

  • to: target step id
  • kind: always | data_complete | intent | condition | tool_result
  • intent (kind=intent), expr (kind=condition)
  • tool (kind=tool_result): fires when it ran successfully this turn

Runtime state (FlowState)

  • flow_id, current_step_id
  • visited: step ids seen this session
  • done: flow reached a terminal step
  • data: answers the flow kept (masked in responses)
  • turns, step_turns: turns in the flow / on this step
  • One FlowState per session, advanced each turn; removed after 90 idle days

Graph validation

  • At least one step; step ids unique
  • start_step_id must be a defined step
  • Every transition target must exist
  • intent / condition / tool_result carry their field
  • All steps reachable from start (BFS)

Bot & group attachment

  • Bot: bots/{botId}.flowId
  • Group: agentGroups/{groupId}.flowId
  • Attach via /attach so the field is not dropped by the model write-path
  • Engine reads flowId off the raw doc per turn

Runtime execution (per turn)

  • 1. Read flowId + load the flow (flow_loader.load_flow)
  • 2. Resolve the session's FlowState (start on first turn)
  • 3. Inject the step goal and what it still needs; strict mode scopes allowed_tools
  • 4. Save each answer — to the profile, or kept by the flow when the bot can't save it
  • 5. advance() evaluates transitions and moves to the next step
  • 6. A terminal step marks the flow done

Versioning

  • Each update snapshots the prior revision
  • GET .../versions lists prior revisions
  • Restore = load a revision into the editor and Save
  • Best-effort — a snapshot failure never blocks the save

Plan limits

  • Every plan has flows — only new ones are capped (max_flows)
  • Free: 1 flow · Lite: 10
  • Pro / Team / Enterprise: unlimited
  • Editing an existing flow is never blocked

RBAC

  • Read: any org member
  • Create / Update / Delete / Attach: requires the flows feature
  • All endpoints run authorize_org (org membership)
  • Written only via engineFetch — never Firestore directly

API endpoints (Python FastAPI, internal)

  • POST /v1/flows/{company_id} — create flow (validates the graph)
  • GET /v1/flows/{company_id} — list ({ flows, total })
  • GET /v1/flows/{company_id}/{flow_id} — get single
  • PUT /v1/flows/{company_id}/{flow_id} — update (snapshots prior revision)
  • GET /v1/flows/{company_id}/{flow_id}/versions — list revisions
  • DELETE /v1/flows/{company_id}/{flow_id} — delete
  • POST /v1/flows/{company_id}/{flow_id}/attach — bind to bot or group
  • POST /v1/flows/{company_id}/detach — unbind from bot or group

All endpoints require authorize_org(request, company_id). Mutations require an admin or owner; creates count against max_flows. Internal (dashboard-managed), not public REST.

Firestore shape

text
{envRoot}/flows/{flowId}
  |- name, description
  |- mode: guided | strict
  |- start_step_id: string
  |- steps: [{ id, name, goal, collect[],
  |            allowed_tools, allowed_agent_id?,
  |            transitions: [{ to, kind, intent?, expr?, tool? }],
  |            terminal }]
  +- enabled: bool

{envRoot}/bots/{botId}
  +- flowId: string | null

{envRoot}/agentGroups/{groupId}
  +- flowId: string | null

Tracked details (state)

Typed details a bot remembers and keeps up to date while it talks — set them up under Bots → your bot → Memory. Each detail is remembered for this conversation, this person (across all their conversations) or everyone (one shared value). The bot sees them in its prompt and changes them with its update_state tool; prompts use {{state.key}}; your systems read and change them with the API below. Every change is a revision you can restore. The schema is edited in Test and released to Production like the rest of the bot; values are runtime data in each environment.

Types and update behavior

  • text, number, boolean, date (ISO 8601), choice, list (of text, numbers or objects), object
  • replace, keep_first — any type
  • increment, max, min — numbers (max/min also dates)
  • append, append_unique — lists
  • merge — objects (RFC 7396: a null removes a key)
  • Optional rules: number range, text length and pattern, list size, JSON Schema (Draft 2020-12) for objects

Control and privacy

  • Who may change each detail: the bot, the API, teammates
  • A connector tool can save values from its answer into details (Connector → Config → Save into tracked details) — they count as coming from the API
  • Flow rules can branch on details: state.issue_type == "Refund"
  • Hide a value from the bot (it can still set it)
  • Sensitive: masked in prompts, traces, the widget and API responses; admins reveal it, and each reveal is audited
  • Forget after N days (person and everyone details)
  • Conversation details follow your conversation retention; person details are erased with the person

API

Use an API key with the state:read or state:write scope. The key's organization and environment apply; a key restricted to one bot reaches only that bot. The subject is a conversation id (session), your end-user id (person) or shared (bot).

GET/api/v1/state/{botId}/configThe bot's tracked details
GET/api/v1/state/{botId}/records?scope=&limit=Recently changed records
GET/api/v1/state/{botId}/records/{scope}/{subjectId}One record's values
PATCH/api/v1/state/{botId}/records/{scope}/{subjectId}Change values (atomic; all or nothing)
GET/api/v1/state/{botId}/records/{scope}/{subjectId}/revisionsHistory, newest first
POST/api/v1/state/{botId}/records/{scope}/{subjectId}/restoreRestore a revision (as a new one)
DELETE/api/v1/state/{botId}/records/{scope}/{subjectId}Delete a record and its history
PUT/api/v1/state/{botId}/configReplace the schema (Test keys)
bash
curl -X PATCH https://rylvo.com/api/v1/state/bot_123/records/person/user_42 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-shipped-9921" \
  -d '{
    "ops": [
      { "field": "plan", "op": "replace", "value": "pro" },
      { "field": "orders", "op": "update", "value": 1 },
      { "field": "coupon", "op": "clear" }
    ],
    "expectedRevision": 7
  }'

update applies the detail's update behavior, replace sets the exact value, clear removes it. values is shorthand for one update per key. expectedRevision makes the write fail with 409 (and the current revision) if anything changed since you read it; a refused value is a 422 with a reason per field. Bot-run responses include the turn's state and stateEvents when the key has a state scope.

Conversations: rules and carry-over

Start a new conversation when…

  • A rule watches a conversation or person detail: it becomes a value, changes, is set or is cleared
  • It fires once, on the change — not while the value stays the same
  • Changed by the bot: the conversation ends after its reply (the bot is told, so it wraps up)
  • Changed by your systems or a teammate: the person's open conversation ends; the response says how many (sessionsEnded)
  • The person's next message starts a new conversation; traces show the rule

Carry into the next conversation

  • Conversation details start empty in every new conversation
  • Mark one to carry over and the next conversation starts with its last value (e.g. the order being handled)
  • Copied once, before the first reply, as its own revision — never over a value already there
  • Person details are always kept across conversations

Memory beyond the last messages

Under Bots → your bot → Sessions, the memory window sets how many recent messages a reply carries. A token budget also keeps them within that many tokens, newest first, so a few long messages can't crowd out the rest. Summarize older messages folds messages that leave the window into a running summary the bot sees on every turn — updated after replies, every few messages, on the bot's own model. Traces show when a reply used the summary or the budget left messages out.

Several messages at once (double texting)

Under Bots → your bot → Sessions, choose what happens when someone sends another message while the bot is still answering:

  • Answer in order (default) — one reply at a time; the next message is answered after the reply before it, with it in mind
  • Wait and answer together — waits a moment (1–15 s) after the last message and answers quick messages in one reply; photos and voice notes are never merged (on the API and Beam, messages are answered in order)
  • Answer only the first — messages sent mid-reply get a short notice on channels; the API answers 409 conversation_busy with Retry-After
  • Answer each at once — no coordination; replies can overlap

Coordination is per end user and conversation, across every server. It never loses a message: a channel message that has waited 45 seconds is answered anyway, and an API call waits up to 25 seconds before conversation_busy. Calls without endUserExternalId aren't coordinated.

Run context

Send facts your app knows with a message — the person's plan, the page they're on — as context on bot-run (a JSON object, up to 16 KB). The bot sees it for that message only, prompts use {{context.plan}} or {{context.account.tier}}, and nothing is stored — traces keep the key names only. Set a JSON Schema under Memory → Run context and a request that doesn't match gets a 422 with the reasons in details.problems, before the bot runs. Context sent from the widget is ignored.

bash
curl -X POST https://rylvo.com/api/v1/bot-run \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "botId": "bot_123",
    "endUserExternalId": "user_42",
    "message": "Can I add a seat?",
    "context": { "plan": "pro", "seats": 4, "page": "/billing" }
  }'

Long-term memory

Tracked details are fields you define. Long-term memory is everything else worth knowing next time — a person's preferences, their situation, what happened — kept as short sentences the bot reads when they matter. Turn it on in the bot's Memory tab (Test, then release).

  • About each person — kept across all of their conversations with the bot (needs endUserExternalId). Shared — true for everyone, like a team notebook; new shared memories the bot saves can wait for a teammate's review.
  • How they're made — the bot saves and corrects them as it talks (its remember and forget tools), and after a conversation ends an AI reads it and saves what's worth remembering, following your instructions. Your app and teammates can add them too.
  • How they're used — on every message the bot is shown the memories closest in meaning to it (pinned ones first), up to the number you choose; it can search the rest.
  • Housekeeping — a new memory that says what an existing one says updates it instead; a person's memories can be forgotten after days unused; erasing a person deletes theirs everywhere.
GET/api/v1/memory/{botId}/memories?scope=person&subjectId=…A person's memories (or scope=shared)
POST/api/v1/memory/{botId}/memoriesRemember something
PATCH/api/v1/memory/{botId}/memories/{memoryId}Correct, pin, tag or approve
DELETE/api/v1/memory/{botId}/memories/{memoryId}Delete one
POST/api/v1/memory/{botId}/searchSearch by meaning
DELETE/api/v1/memory/{botId}/people/{subjectId}Forget everything about a person
bash
# Tell the bot something about a customer, from your backend
curl -X POST https://rylvo.com/api/v1/memory/bot_123/memories \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"scope": "person", "subjectId": "user_42", "text": "Upgraded to the Pro plan on 3 Oct 2026.", "kind": "event"}'

# What does the bot remember about their billing?
curl -X POST https://rylvo.com/api/v1/memory/bot_123/search \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"query": "billing and plan", "subjectId": "user_42"}'

Use a key with memory:read or memory:write. With memory:read, bot-run replies also carry memory: the memories the bot was shown and what it changed. A full space answers 409; when long-term memory is off, writes answer 422.

Custom code

Run your own code at five points of every conversation, from the bot's Code tab (Test, then release): on your server — Rylvo POSTs JSON to your HTTPS endpoint, signed — or on Rylvo — you write a JavaScript module and Rylvo runs it in a sandbox (fresh per call, your time limit, 32 MB, fetch only to hosts you allow, secrets as ctx.secrets.NAME). Either way Rylvo uses what you answer. Every field of an answer is optional — an empty {} changes nothing.

StageWhenYou may answer
on_session_startFirst message of a conversationcontext, instructions, state
before_turnEvery message, before the modelcontext, instructions, state, reply, block
retrieveEvery message, alongside the knowledge basepassages
after_turnAfter the bot replies, before it's sentreply, state, endConversation
on_session_endA conversation ended (retried)— (answer ignored)
  • context — facts the bot sees for this message (joins the run context); instructions — extra rules for this reply.
  • state — tracked-detail values to save, as your systems (the API writer), e.g. {"plan": "pro"}.
  • reply — answer yourself (no model call) or, after the turn, replace the bot's reply; block — refuse with your message.
  • passages — up to 10 {title, text, url?, score?} the bot grounds its answer on, cited like knowledge-base excerpts.
  • endConversation — true or {"reason": "…"}: the person's next message starts a new conversation.
json
// What your endpoint receives (before_turn)
{
  "stage": "before_turn", "deliveryId": "msg_…", "test": false,
  "bot": { "id": "bot_123", "name": "Support" }, "environment": "production",
  "conversation": { "id": "conv_8f2a91", "channel": "widget" },
  "person": { "id": "user_42" },
  "message": { "text": "Where is my order A-1042?" },
  "transcript": [{ "role": "user", "text": "Hi" }, { "role": "assistant", "text": "Hello!" }],
  "state": { "session": { "order_id": null }, "person": { "plan": "free" } },
  "context": { "page": "/orders" }
}

Calls are signed with the bot's signing secret (one per environment, on the Code tab) using the Standard Webhooks scheme — webhook-id, webhook-timestamp, webhook-signature — so any Standard Webhooks library verifies them. Answer within the stage's time limit (500 ms–10 s); when your endpoint fails, the bot carries on without it, or — if you chose so for before/after — sends your fallback message. Internal addresses are never called. Every call shows as a “Your code” step in the trace.

javascript
// On Rylvo: the whole stage is this module
export default async function (event, ctx) {
  const r = await fetch("https://api.yourshop.com/orders?person=" + event.person?.id, {
    headers: { authorization: "Bearer " + ctx.secrets.SHOP_KEY },
  });
  const [order] = await r.json();
  return order ? { context: { order_status: order.status, eta: order.eta } } : {};
}

On your server, verify the signature first:

javascript
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.RYLVO_SIGNING_SECRET); // whsec_…

app.post("/rylvo/stage", express.raw({ type: "application/json" }), (req, res) => {
  const event = wh.verify(req.body, req.headers); // throws if forged or stale
  if (event.stage === "before_turn" && /order/i.test(event.message.text)) {
    const order = lookUpOrder(event.person?.id);
    return res.json({ context: { order_status: order.status, eta: order.eta } });
  }
  res.json({});
});

Sessions API

A bot's conversations, for your backend: list and search them — by person, by whether they've ended, or by their tracked details — read one with its transcript, end one, or fork one into a copy to continue from. Use an API key with the sessions:read or sessions:write scope; a key restricted to one bot reaches only that bot.

GET/api/v1/sessions?botId=…Newest first; endUserExternalId, status=open|ended, state.<key>=<value>, limit, before
GET/api/v1/sessions/{sessionId}?botId=…The conversation and its transcript
POST/api/v1/sessions/{sessionId}/endEnd it; the person's next message starts a new one
POST/api/v1/sessions/{sessionId}/forkA copy with the transcript and tracked details
bash
# Open conversations about a refund
curl "https://rylvo.com/api/v1/sessions?botId=bot_123&status=open&state.issue_type=Refund" \
  -H "Authorization: Bearer YOUR_API_KEY"

# End one, leaving a note the next conversation can carry
curl -X POST https://rylvo.com/api/v1/sessions/conv_8f2a91/end \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"botId": "bot_123", "note": "Refund issued; tracking number sent."}'

# Fork it from an earlier point to try a different reply
curl -X POST https://rylvo.com/api/v1/sessions/conv_8f2a91/fork \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"botId": "bot_123", "throughMessageId": "m_41", "stateRevision": 3}'

A fork has no channel of its own, so nothing reaches anyone until you send a message to it with bot-run and its sessionId — with the same end user it then becomes their current conversation. Ending is idempotent; the note becomes the close summary, carried into the next conversation when the bot's continuity is "profile + summary".

DatasetsDashboard-only

Rylvo Datasets walkthrough

Datasets are curated collections of traces or end-user profiles for analysis, review, and team collaboration. Manage from Dashboard → Datasets.

Trace datasets

Pin canonical traces into a shared collection for debugging, regression testing, or qualitative review.

  • Pin from the Traces page or browse unpinned traces inside the dataset
  • Trace source: canonical, legacy, or dashboard
  • Inline trace replay + detail drawer
  • Shared comment thread with the Traces page
  • Soft-archive or hard-delete a dataset

People datasets

Group end-user profiles for segmentation, cohort review, or export.

  • Collect end-user profiles from Audience
  • Read-only detail view per profile
  • Reuse for broadcast audience targeting

Retention & quotas

PlanMax datasetsMax preserved traces
Free150
Lite105,000
Pro5025,000
Team5025,000
Enterprise∞∞

Permissions

  • Viewer+ — view datasets
  • Operator+ — create, edit, archive, pin/unpin traces
  • Admin — hard-delete dataset and all pinned items

Deleting a dataset removes the pins and the dataset doc. Pinned traces themselves are then subject to the normal org retention policy, so delete carefully.

Email DeliveryDashboard-only

Rylvo handles its own transactional emails (welcome, team invites, scheduled reports, operational alerts) automatically. Templates are branded and theme-aware - there is nothing for you to set up.

To send your own emails on workflow events (e.g. notify a human when a session escalates), use an Event Connector or add an Email channel in Automation.

OAuth ClientsDashboard-only

Server-to-server OAuth 2.1 authorization server for MCP (Model Context Protocol) client authentication. Enables AI editors (Claude Code, Cursor, Windsurf, Zed) to authenticate with Rylvo and access the MCP tools API. Implements RFC 6749, 7636 (PKCE), 7591 (DCR), 7009 (revocation), 7662 (introspection), 8414 (metadata), 8707 (audience binding), 9728 (protected resource). Manage from Dashboard → Integrations.

Client registry document

  • clientId: "client_" + 12 random base64url chars
  • clientName (1-200), redirectUris[]
  • grantTypes: authorization_code, refresh_token
  • tokenEndpointAuthMethod: none | client_secret_post
  • clientSecretHash: SHA-256 hash (null for public clients)
  • Global collection (not org-scoped); plaintext secret never persisted

Client types & secret management

  • Public (PKCE-only): no secret issued, authMethod="none"
  • Confidential: 32-byte random base64url secret, authMethod="client_secret_post"
  • Secret returned ONCE at DCR response; only SHA-256 hash stored
  • Constant-time comparison (timingSafeEqual) prevents timing attacks

Grant types & token flow

  • Authorization Code (PKCE required): authorize → consent → token exchange
  • Refresh Token Rotation: atomic Firestore transaction (revoke old, issue new)
  • Org ID embedded in self-describing token: rfr_{base64url(orgId)}~{random}
  • CSRF protection: HMAC-SHA256 on consent form

Token lifetimes

  • Access token: 24 hours (RS256 JWT, aud-bound to https://rylvo.com/mcp)
  • Refresh token: 30 days sliding window (rotated on each use)
  • Refresh token family ceiling: 365 days absolute
  • Authorization code: 10 minutes

Scopes (3)

  • rylvo:read — view bots, analytics, conversations, KB
  • rylvo:write — create/update bots, prompts, knowledge, settings
  • rylvo:admin — manage org settings, members, integrations
  • Default: "rylvo:read rylvo:write"

Security features

  • RS256 JWT signing with key versioning (OAUTH_JWT_PRIVATE_KEY_PEM in prod)
  • PKCE S256 required (plain rejected in production)
  • Audience binding (RFC 8707) prevents confused-deputy attacks
  • Environment binding: a grant authorizes one environment, chosen at consent and carried in the token; refresh cannot re-negotiate it
  • Headers: HSTS, X-Frame-Options DENY (widget excepted), CSP report-only
  • Revoked JTI tracking: Firestore + in-memory LRU (max 10k)

Caching strategy

  • Client metadata: 5 min TTL, max 5,000 entries
  • JWT validation: negative (5s spam) + positive (2s revocation-aware), max 10k each
  • Firebase session: 30s TTL, max 2,000 entries
  • JTI revocation: in-memory LRU, max 10,000 entries

RBAC & plan limits

  • oauthClients.view — viewer+ (all members)
  • oauthClients.revoke — admin+ (admin or owner)
  • OAuth collections are server-only (Admin SDK writes)
  • NOT plan-gated — available to all plans (free → enterprise)
  • Revocation filters oauthRefreshTokens.clientId per collection — automatic single-field index covers it

API endpoints (OAuth 2.1 standard)

  • GET /.well-known/oauth-authorization-server — RFC 8414 metadata (30/min per IP)
  • GET /.well-known/jwks.json — RFC 7517 JWKS
  • GET /oauth/authorize — authorization endpoint (consent HTML, 30/min per IP)
  • POST /oauth/authorize/consent — consent submission (15/min per user)
  • POST /oauth/token — token endpoint (10/min per client_id)
  • POST /oauth/register — Dynamic Client Registration (5/min per IP)
  • POST /oauth/revoke — token revocation (10/min per IP)
  • POST /oauth/introspect — token introspection (30/min per IP)

Dashboard: GET /api/v1/oauth-clients/{orgId} (list), POST /api/v1/oauth-clients/{orgId}/{clientId}/revoke (admin+).

Firestore shape

text
oauthClients/{clientId}  (global, not org-scoped)
  |- clientId, clientName (1-200), redirectUris[]
  |- grantTypes[], tokenEndpointAuthMethod, scope
  |- clientSecretHash (SHA-256 | null for public)
  +- createdAt: number (unix seconds)

oauthAuthCodes/{codeHash}  (keyed by SHA-256 hash)
  |- orgId, userId, clientId, redirectUri, scope
  |- codeChallenge, codeChallengeMethod
  |- expiresAt (10 min TTL), used, consumedAt?
  +- createdAt

environmentRoot(orgId, environment)/oauthRefreshTokens/{tokenHash}
organizations/{orgId}/oauthRefreshTokens/{tokenHash}  (unbound legacy)
  |- userId, clientId, scope
  |- environment: test | production | null (grant binding)
  |- expiresAt (30d sliding), familyMaxExpiresAt (365d ceiling)
  |- revoked, revokedAt?
  +- createdAt
  Token format: rfr_{base64url({o:orgId,e:environment})}~{random}

organizations/{orgId}/oauthRevokedJtis/{jti}
  |- revokedAt
  +- expiresAt (24h, access token TTL)

TTL policies REQUIRED: authCodes (10m), refreshTokens (30d), revokedJtis (24h)

Webhook Connectors

Rylvo Connectors walkthrough

Connectors let Rylvo call your systems during workflow execution. Register webhook endpoints for tool calls, state sync, or event notifications - Rylvo handles auth, retry, and signing.

Three connector types

Tool

Execute actions on your systems (create case, look up order, check inventory)

State Sync

Read/write workflow state from your DB before and after decisions

Event

Receive notifications on escalation, stage change, verification failure

Register a tool connector

bash
curl -X POST https://rylvo.com/api/v1/connectors \
  -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "company_id": "comp_001",
    "connector_type": "tool",
    "name": "Salesforce Case Creator",
    "auth": {
      "auth_type": "api_key",
      "api_key_header": "X-Salesforce-Key",
      "api_key_value": "your_salesforce_api_key"
    },
    "tool_config": {
      "tool_name": "create_salesforce_case",
      "tool_description": "Create a case in Salesforce",
      "input_schema": { "type": "object", "properties": { "issue_summary": { "type": "string" } } }
    },
    "endpoint": { "url": "https://your-app.com/rylvo/tools/create-case", "method": "POST" }
  }'

API endpoints

MethodPathDescription
POST/v1/connectorsCreate connector
GET/v1/connectors/{company_id}List connectors
GET/v1/connectors/{company_id}/{id}Get detail
PATCH/v1/connectors/{company_id}/{id}Update
DELETE/v1/connectors/{company_id}/{id}Delete
POST.../{id}/activateActivate
POST.../{id}/deactivateDeactivate
POST.../{id}/testTest endpoint
POST/v1/connectors/generate-secretGenerate HMAC secret
GET/v1/connectors/{company_id}/tools/availableList webhook tools

7 authentication methods

noneapi_keybearer_tokenhmac_sha256basicoauth2_client_credentialscustom_header

11 subscribable event types

lead.capturedworkflow.decision.completedworkflow.decision.failedworkflow.escalation.triggeredworkflow.stage.changedtool.execution.completedtool.execution.failedstate.updatedverification.failedsession.startedsession.closed

Send every lead to your CRM

When an assistant captures an email address or phone number, Rylvo records the lead, emails the workspace owners, and fires lead.captured to every event connector subscribed to it. Create an Event connector, tick Lead Captured, and paste the URL of a Zapier “Catch Hook”, a Make custom webhook, an n8n Webhook node or a GoHighLevel inbound-webhook trigger. Map the fields once; every new lead lands there as a contact.

{
  "event_id": "evt_lead_…",          // stable per lead — dedupe on it
  "event_type": "lead.captured",
  "bot_id": "…",
  "data": {
    "lead_id": "…",
    "email": "casey@example.com",
    "phone": null,
    "name": "Casey Morgan",
    "fields": { "email": "…", "full_name": "…", "need": "…" },
    "first_message": "Do you do implants?",
    "bot_name": "Acme Dental Assistant",
    "channel": "widget",
    "conversation_id": "…",
    "lead_url": "https://rylvo.com/dashboard/leads",
    "captured_at": "2026-09-23T10:00:00.000Z"
  }
}

Deliveries are signed (Standard Webhooks), retried, and replayable from the connector’s delivery log. Leads captured in the Test environment are never delivered.

Connector Python SDK

Typed convenience methods for all connector operations. Sync and async variants included.

python
from core.connectors.sdk import ConnectorSDK

sdk = ConnectorSDK(base_url="https://rylvo.com", api_key="rylv_...")

# Register a tool connector
connector = sdk.create_tool_connector(
    company_id="comp_001",
    name="Salesforce Case Creator",
    endpoint_url="https://your-app.com/rylvo/tools/create-case",
    tool_name="create_salesforce_case",
    tool_description="Create a case in Salesforce",
    auth_type="api_key",
    api_key_header="X-Salesforce-Key",
    api_key_value="your_salesforce_api_key",
)

# Test + activate
test = sdk.test_connector("comp_001", connector["connector_id"])
if test["success"]:
    sdk.activate("comp_001", connector["connector_id"])

# List available tools
tools = sdk.list_available_tools("comp_001")
print(f"{tools['total']} webhook tools available")

Async variant

python
from core.connectors.sdk import AsyncConnectorSDK

async with AsyncConnectorSDK(base_url="...", api_key="...") as sdk:
    connector = await sdk.create_event_connector(
        company_id="comp_001",
        name="Slack Alerts",
        endpoint_url="https://hooks.slack.com/...",
        event_types=["workflow.escalation.triggered"],
    )
    await sdk.activate("comp_001", connector["connector_id"])

Error handling

ConnectorNotFoundError(404)Connector does not exist
ConnectorAuthError(401/403)Invalid or missing API key
ConnectorValidationError(422)Invalid connector configuration
ConnectorSDKError(5xx)Base class for all SDK errors

ChannelsDashboard-only

Rylvo Channels walkthrough

Multi-channel bot deployment configuration. 13 channel types supported — deploy your bot to website widgets, WhatsApp, Slack, Telegram, Discord, MS Teams, Messenger, Instagram, SMS, LINE, Email, or custom webhooks. Multiple configs per bot are allowed, but only one enabled per channel type. Includes health monitoring, secret encryption, webhook idempotency, and race-condition protection. Manage from Dashboard → Deploy.

13 channel types

  • widget — Website chat (origins, accent color, greeting, position, branding)
  • whatsapp — Meta Cloud API (phoneNumberId, verifyToken)
  • slack — (teamId, botUserId, signingSecret, channels[])
  • telegram — (botId, webhookSecret, allowedChatIds, collectFeedback)
  • discord — (applicationId, publicKey, guildId, commandName)
  • msteams — (appId, appPasswordPrefix, tenantId)
  • messenger — (pageId, verifyToken, appSecretPrefix)
  • instagram — (igUserId, verifyToken, appSecretPrefix)
  • sms_twilio — (accountSid, fromNumber, messagingServiceSid?)
  • whatsapp_twilio — (same as SMS, connector adds whatsapp: prefix)
  • line — (channelId, channelSecret, channelAccessToken, botUserId)
  • email_inbound — (inboxAddress, fromAddress, replyMode, Resend)
  • custom — (inboundSecret, label?)

Setup status state machine

  • needs_setup → verified, deprecated (incomplete; cannot enable)
  • verified → live, deprecated (test passed; can enable)
  • live → verified, deprecated (enabled and receiving)
  • deprecated → verified, needs_setup (restorable)
  • enabled only allowed when setupStatus in [verified, live]
  • First successful test auto-promotes to live (if no sibling enabled)

One-enabled-per-type rule

  • Enforced at API layer via Firestore transactions (runTransaction)
  • On create/patch with enabled=true: query for existing enabled config with same botId + channelType
  • Returns 409 Conflict if duplicate found
  • Firestore rules enforce immutability of botId and channelType

Health monitoring

  • Status: ok | error | idle
  • 3+ consecutive errors flips status to "error"
  • Notification fanout on first error and at 3-in-a-row; 15-min cooldown
  • Append-only event log (last 10 events, ring buffer) eliminates RMW hotspot
  • Tracks inboundCount, outboundCount, errorCount, consecutiveErrors

Secret storage & redaction

  • Full tokens in env vars (e.g. TELEGRAM_BOT_TOKEN_{prefix})
  • Encrypted tokens in Firestore as AES-256-GCM v1: blobs
  • Token prefixes (first 8 chars) stored in plaintext for display
  • GET responses strip encrypted fields before sending to client
  • Resolution order: env var → encrypted Firestore → legacy plaintext

Webhook idempotency

  • 24-hour fingerprint cache at /dedup/{fingerprint}
  • Format: {channel}:{platform_id} (e.g. telegram:12345)
  • Platform retries are safe — duplicates silently ack'd
  • TTL: 24 hours (Firestore TTL policy)

Inbound webhook flow (14 steps)

  • 1-4: Buffer body, parse, early ack, pre-config signature check
  • 5-8: Kill switch, channel lookup, org suspension, per-org rate limit (60 cap / 120/min)
  • 9-12: Post-config ack, parse to NormalizedInbound, session resolution, dedup check
  • 13-14: Record inbound, execute bot turn + send reply (3-retry) + record outbound/error
  • All synchronous within webhook request (15s timeout)
  • Discord special case: returns type:5 (thinking) immediately, runs in background

RBAC & plan limits

  • channels.view — viewer+ (read)
  • channels.manage — admin+ (create/update/delete)
  • channels.test — operator+ (test connection)
  • channels.rotate_secret — admin+ (rotate secrets)
  • No per-plan channel limits — unlimited per plan
  • Health subcollection is read-only for clients (server-only writes)

API endpoints (BFF Next.js)

  • POST /api/channels — create channel config (admin)
  • GET /api/channels?orgId=…&botId=…&channelType=…&enabled=… — list (viewer)
  • GET /api/channels/{configId} — get single (viewer)
  • PATCH /api/channels/{configId} — update (admin)
  • DELETE /api/channels/{configId} — delete (admin)
  • POST /api/channels/test — test connection (operator)

Webhook endpoints: /api/channels/{telegram,whatsapp,slack,discord,sms,whatsapp_twilio,line,messenger,instagram,msteams,email_inbound,custom}

Firestore shape

text
{envRoot}/botChannelConfigs/{configId}
  |- botId (immutable), botName, channelType (immutable)
  |- enabled, requireExplicitConsent, defaultConsentScope
  |- config: <channel-specific config object>
  |- setupStatus: needs_setup | verified | live | deprecated
  |- lastTestedAt?, lastVerifiedAt?, apiKeyId?
  |- createdBy, createdAt, updatedAt

{envRoot}/botChannelConfigs/{configId}/health/main
  |- status: ok | error | idle
  |- lastInboundAt, lastOutboundAt, lastErrorAt
  |- lastErrorMessage (500 chars max)
  |- inboundCount, outboundCount, errorCount, consecutiveErrors
  |- recentEvents[]: { ts, kind: inbound|outbound|error, summary } (last 10)
  |- updatedAt, lastNotifiedAt?

{envRoot}/botChannelConfigs/{configId}/dedup/{fingerprint}
  |- createdAt, expireAt (24h TTL)

{envRoot}/botChannelConfigs/{configId}/health/events/log/{docId}
  |- (append-only event log, eliminates RMW hotspot)

{envRoot}/channelMediaAssets/{assetId}
  |- expiresAt (max 30 days), GCS blob reference

Image & Voice IntakeDashboard-only

Customers can send photos, screenshots, documents and voice notes, and the bot reads and hears them. Extraction runs on your own LLM key, like the rest of the intelligence layer — there is no per-file markup.

Turning it on

It is off by default on every new bot. Enable images and audio per bot in the bot editor, and set workspace-wide rules in Settings → Media Intake: which attachment types are allowed at all, size and length caps, how hard personal data is scrubbed from transcripts, and whether customer files may be stored at rest.

Where it works

Every deployed channel — Telegram, WhatsApp (Cloud API and Twilio), Slack, Discord, MS Teams, Messenger, Instagram, LINE, SMS/MMS and inbound Email — plus the website widget, the dashboard chat, and the attachments parameter on /v1/bot-run.

How each type is read

  • Images and screenshots — a vision model transcribes visible text and describes what is in the picture.
  • Voice notes — transcribed to text, then handled like any other message. Replies are text; the bot does not speak back.
  • Documents — PDFs, Word, PowerPoint and spreadsheets are read from their own text layer, which is exact and costs nothing. A scanned document with no text layer falls back to the vision model.

What it costs

Each plan includes a monthly allowance of images and voice minutes (see pricing). Audio is billed in whole minutes, rounded up per clip. Reading a document's text layer uses no allowance at all. The same file sent twice is only processed once.

Handling and limits

  • Location and device metadata is stripped from photos before they are stored.
  • Personal data is scrubbed from extracted text before it is stored or sent to a model. Names are not removed — that needs name recognition we do not run.
  • Stored files expire on your plan's retention window, and extracted text expires with them.
  • Text found inside an image is treated as data, never as instructions to the bot.
  • 25 MB per attachment. Long recordings may be answered in a follow-up message.

MCP HubDashboard-only

Rylvo MCP Hub walkthrough

The MCP Hub is Rylvo's Model Context Protocol registry - a curated catalog of MCP servers your bots can use as tools. Manage from Dashboard → MCP Hub.

What you can do

  • Discover & install MCP servers
  • Connect external servers with HTTP, SSE, or stdio transports
  • Store credentials securely (encrypted at rest)
  • Allow specific servers per bot
  • See invocation logs & per-server analytics
  • Submit your own server for review

Built-in controls

  • Approval workflow before a server can be used in production
  • Outbound OAuth 2.1 (PKCE + DCR) for user-facing servers
  • Cost tiers (low / medium / high) surfaced before invocation
  • Health checks & auto-disable on repeated failure
  • Resume tokens for long-running tool calls

Build Intelligence: You can also expose the entire Rylvo workspace as an MCP server to Claude Code, Cursor, Windsurf, Zed, or Codex. See the Build Intelligence section below or visit /build-intelligence.

Build IntelligenceDashboard-only

Rylvo Build Intelligence walkthrough

Build Intelligence lets AI editors and agents control Rylvo directly. Connect Claude Code, Cursor, Windsurf, Zed, or Codex CLI via MCP and give them 160+ tools to build bots, prompts, guardrails, skills, knowledge-base connections, connectors, channels, and more. Manage the connection from Dashboard → Build Intelligence or visit /build-intelligence.

Supported clients

  • Claude Code — remote HTTP install command + /mcp authorization
  • Cursor — mcp.json remote server configuration
  • Windsurf — mcp_config.json remote server configuration
  • Zed — context_servers settings entry
  • Codex CLI — codex mcp add + codex mcp login
  • Other Streamable HTTP clients — verify OAuth support with the client

Security model

  • OAuth 2.1 with PKCE (S256) — browser flow on first install
  • RS256 JWT access tokens, 24-hour TTL
  • Refresh tokens rotate on every exchange
  • Every call is org-scoped: token org_id must match the resource
  • A leaked token can act only in its bound org until expiry or revocation

Install examples

shell
# Claude Code
claude mcp add --transport http rylvo https://rylvo.com/mcp
# Then run /mcp inside Claude Code and authenticate
json
// Cursor — .cursor/mcp.json or ~/.cursor/mcp.json
{
  "mcpServers": {
    "rylvo": { "url": "https://rylvo.com/mcp" }
  }
}
json
// Windsurf — ~/.codeium/windsurf/mcp_config.json
{
  "mcpServers": {
    "rylvo": { "serverUrl": "https://rylvo.com/mcp" }
  }
}
json
// Zed — settings.json
{
  "context_servers": {
    "rylvo": { "url": "https://rylvo.com/mcp" }
  }
}
shell
# Codex CLI
codex mcp add rylvo --url https://rylvo.com/mcp
codex mcp login rylvo --scopes rylvo:read,rylvo:write

Tool surface

ResourceReadWriteDelete
Botsbots.list, bots.getbots.create, bots.updatebots.delete
Promptsprompts.list, prompts.getprompts.create, prompts.update, prompts.set_optimizationprompts.delete
Guardrailsguardrails.list, guardrails.getguardrails.create, guardrails.updateguardrails.delete
Teststests.list_cases, tests.list_runs, tests.get_run_resultstests.create_case, tests.run_suitetests.delete_case
Connectorsconnectors.list, connectors.getconnectors.create, connectors.update, connectors.testconnectors.delete
Channelschannels.list, channels.getchannels.create, channels.update, channels.register_webhook—
MCP Serversmcp_servers.list, mcp_servers.getmcp_servers.register, mcp_servers.updatemcp_servers.delete
Skillsskills.list, skills.getskills.create, skills.updateskills.delete
Knowledge Basekb.list_connections, kb.get_connectionkb.create_connection, kb.update_connection, kb.add_sourcekb.delete_connection
Agent Groupsagent_groups.listagent_groups.create, agent_groups.add_botagent_groups.delete
Automationsautomations.list_tasks, broadcasts.listautomations.create_task, broadcasts.createautomations.delete_task, broadcasts.delete

Macro tools

  • scaffold_bot_from_description — create a bot + system prompt + starter guardrail from a brief
  • clone_bot — duplicate a bot and optionally copy linked resources
  • attach_mcp_server — register an external MCP server, write its tools, and link it to bots

Entitlements still apply: MCP does not bypass product access or quotas. Each tool action is checked against its scope, workspace permissions, plan availability, validation, and the same limits shown in Billing & Usage.

Workspace ArchitectDashboard-only

Rylvo Workspace Architect walkthrough

The Architect is a conversational setup agent that turns a plain-English brief into a working workspace: bots, prompts, guardrails, connectors, KB connections, evolution rules, and tests. It runs at /dashboard (Architect tab) and also powers natural-language edit mode and bot cloning.

8-phase agent loop

1. Intent

Capture goal, industry, tone, compliance constraints

2. Research

Pull org profile defaults, relevant industry templates, prior sessions

3. Blueprint

Propose bots + workflow + guardrails + KB + connectors

4. Review

User approves / edits the blueprint in natural language

5. Generate

Write prompts, guardrails, connectors, rules, KB stubs

6. Verify

Auto-run bot tests on the new bot(s) and score readiness

7. Apply

Commit as versioned resources, tagged source=architect

8. Handoff

Summarise what was built, link to Mission Control & Playground

Other architect capabilities

  • Edit mode - change an existing bot with a sentence ("make it more empathetic, prefer email over phone")
  • Bot cloning - duplicate a bot with modifications (industry, language, tone)
  • Bot testing - architect can run tests and iterate until a readiness threshold is met
  • Industry templates - 29 starting points (SaaS support, fintech KYC, healthcare intake, etc.)
  • Org profile prefill - reuses defaults you set once in your organization profile

Knowledge BaseDashboard-only

Rylvo Knowledge Base walkthrough

Connect your data sources to Rylvo for grounded, evidence-backed decisions. The Knowledge Base offers eight available retrieval blueprints, from Classic RAG and Hybrid Search to Self-RAG, CRAG, and Agentic RAG.

How it works

1

Add a source

Upload documents, connect APIs, or point to URLs. 16 source types supported.

2

Pick a blueprint

Choose from eight available retrieval strategies, including Hybrid Search, HyDE, and Self-RAG.

3

Create a connection

A connection links a source to a blueprint with specific pipeline settings.

4

Query in playground

Test retrieval quality before going live. Tune chunking, reranking, and thresholds.

Built-in blueprints

FAQ RetrievalPolicy LookupTroubleshooting GuideProduct CatalogLegal / ComplianceOnboarding DocsMedical ReferenceFinancial AdvisoriesHR HandbookTechnical ManualsSales PlaybookCustom Pipeline

Dashboard features

Connections

Create, monitor, pause, and delete knowledge base connections

Sources

Manage 16 source types - upload, cloud storage, SaaS, databases, API, web crawl

Blueprints

Pre-built and custom retrieval pipeline configurations

Playground

Test queries, inspect retrieved chunks, tune settings live

Performance

Retrieval quality metrics, latency, and hit rates

Settings

Auto-tune, chunking config, reranking, and threshold tuning

Retrieval stack

1

Vector store

Qdrant stores chunked embeddings. Keyword + semantic search are combined, then reranked for relevance.

2

BYOK decryption

If a source or org uses a BYOK provider key, it is decrypted at retrieval time before calling the embedding model.

3

Retrieval trace

Every KB call is recorded in the canonical observability store as one clean retriever observation per connection, with latency, score, and strategy.

4

Citations

Grounded answers include source references with chunk IDs so you can audit what the bot actually saw.

5

Self-RAG grading (opt-in)

TypeSafe Jev grades each retrieved passage relevant or not in one request and drops the rest; if it would reject every passage, all are kept. It adds a network round trip but no longer spends your model tokens. If Jev cannot answer, the LLM grader runs on your OpenAI key.

Conversation StorageDashboard-only

Rylvo Conversation Storage walkthrough

Bring Your Own Database (BYODB). Every conversation event Rylvo captures — each message, collected answer, checkpoint and session summary — is delivered to a database or webhook you own, so your conversations live permanently in infrastructure you control. Events go out on the delivery run every 15 minutes; with no connection yet, up to 500 wait for one. Manage from Dashboard → Conv. Storage.

Connections

Add one or more connections. Each receives the events you choose, from the bots you choose.

  • PostgreSQL, Supabase, Neon, PlanetScale
  • MySQL, Turso, MongoDB, Redis
  • Webhook (none / API key / Bearer / HMAC auth)
  • Credentials encrypted at rest; check a saved connection any time

Tables & columns

Each bot writes with a template — its tables, and the column each value goes in.

  • Nine templates, or a connection's own columns and custom fields
  • See exactly which columns every event writes
  • Create the tables in one click, or copy the SQL — Rylvo never alters a table

Live saves, delivery history, failed events

Every conversation event by event, and every delivery with its result.

  • Why anything is still waiting, per event
  • Webhook retries within a run (0.5 s → 1 s → 2 s), then the next run
  • Inspect the exact payload that was sent
  • After 10 failed runs, events move to Failed events — replay or discard them
  • Test and Production are separate — follow the environment switch

Retention

How long Rylvo keeps its delivery history (each record carries the event's content). Extend it with a retention add-on, or shorten it.

Lite14 days
Pro90 days
Team90 days
EnterpriseUnlimited

Production / Test isolation

Every storage event carries a source discriminator:channel (live), playground (dashboard chat), or test_suite (automated eval). Live traffic lands in Production and dashboard chats in Test — each with its own connections, queue and history. Every Conversation Storage page follows the environment switch.

Live (channel)

  • Deployed-channel traffic: Telegram, WhatsApp, widget, API, etc.
  • Writes to base tables (e.g. rylvo_conversations)
  • Uses the production circuit-breaker key
  • Counted against your plan's storage events

Test (playground)

  • Dashboard Bot Chat / Group Chat turns
  • Writes to suffixed tables (default *_test)
  • Configurable per-target suffix or optional testDatabaseName
  • Separate circuit-breaker key — missing test tables never affect live dispatch
  • Webhook payloads include source and isTest
  • Delivered only when Allow verified Test deliveries is on (Settings → Organization) — otherwise held, unbilled

Create the tables first. Rylvo only inserts rows; it never creates tables by itself. Each database connection's Tables tab has the exact statements — for Production, and in Test the matching *_test tables — and runs them with the saved credentials.

Your database is the source of truth. Rylvo's copy is an operational cache — not a backup. Add a connection to keep your conversations permanently in your own database. Extending Rylvo-side retention increases storage costs but does not replace your own database.

Audience HubDashboard-only

Rylvo Audience Hub walkthrough

The central operator workspace for managing every end-user across every bot in your organization. Replace scattered user lists with a scalable hub: paginated people browsing, saved filter segments, aggregate analytics, consent governance, CSV import/export, and retention policy controls. Manage from Dashboard → Audience.

People

Paginated, server-driven list of end-users with 50-row default and 200-row maximum cursor pages.

  • Filter by channel, consent scope, forgotten status, or free-text search
  • Bot-scoped filtering with data-collection field predicates
  • Full person page with Overview, Sessions, Profiles, Activity, and Audit tabs
  • Bulk tags, consent, sessions, removal, export, merge, and eligible broadcast actions
  • Anonymous widget captures and test identities stay separate from production People
  • Keyboard shortcuts: / search, j/k navigate, Enter inspect

Audience Rooms

Saved filter presets that act like persistent views. Create rooms for common cohorts and share them with your team.

  • Save any filter combination as a named room
  • Dirty indicator when current filters diverge
  • Pins & Discussion panel per room
  • Deep-linkable via ?room= URL param

Segments

Reusable filters that resolve into concrete member lists for broadcasts and exports.

  • Basic predicates: all users, active within N days, by channel, min conversations
  • Profile predicates: checkpoint achieved, field value, completeness threshold
  • Live preview with match count before saving
  • One-click link to bot broadcast tab

Insights

Cached aggregate analytics computed from a snapshot (6h TTL). Org-wide or bot-scoped.

  • Onboarding funnel, channel mix, consent breakdown
  • Engagement depth, activity recency, hour-of-day heatmap
  • Field-fill rates, top tags, completeness histogram
  • Bot leaderboard (org-wide only)

Imports & Exports

Bring legacy CRM contacts in and export segments for your data warehouse.

  • CSV import wizard: upload → map columns → preview → commit (idempotent)
  • Segment export to CSV (up to 5,000 rows)
  • 15 MB file limit plus a plan-specific row limit

Consent & Privacy

Govern how end-user data is collected and retained.

  • Consent scope tiles: none, short_term_only, personalize, full
  • GDPR forgotten-users queue with deep-links
  • Retention policy: effective days, shorten override, extend add-ons
  • The scheduled retention sweep purges expired records

Profiles & Schema

Org-wide field catalog + per-bot schema cards. Spot naming drift before it ruins dashboards.

Settings

Retention policy, add-on tiers, and governance. Admin-only configuration.

People DatasetsDashboard-only

Named collections of end-users (not traces). Stored in the same datasets collection as trace datasets, discriminated by kind === "people". Created from Audience Room pins or directly from the People page. No retention/export-lock machinery (unlike trace datasets). Quota-enforced via maxDatasets plan limit. Manage from Dashboard → Datasets → People.

Dataset document

  • name (1-200 chars), description (max 2000)
  • kind: "people" (discriminator; legacy docs without kind = "trace")
  • peopleCount: denormalized end-user count
  • retentionPolicy: { mode: "inherit" } (not enforced for people)
  • archived: soft-delete flag

Item document

  • externalId (1-500 chars, end-user identifier)
  • source: "audience_room" | "people_page"
  • addedBy, addedByName, addedAt
  • snapshot: channel, displayName, consentScope, turnCount, lastSeenAtMs, firstSeenAtMs
  • Doc ID = externalId → add/remove is idempotent

Kind discriminator: people vs trace

  • people: groups EndUserDoc profiles, no retention enforcement, no export lock
  • trace: groups TraceView traces, active retention, export lock
  • Both in same Firestore collection (datasets)
  • Filtering: .filter(r => r.data.kind === "people")

3 creation flows

  • From Audience Room pins: createPeopleDatasetFromRoomPins() — fetches all pins, source="audience_room"
  • From People Datasets page: create empty dataset, add people individually
  • From People page: addPeopleToDataset() bulk add, source="people_page"

Quota enforcement (maxDatasets)

  • Free: 1 | Lite: 10 | Pro: 50 | Team: 50 | Enterprise: unlimited (-1)
  • Transactional count check (race-safe inside Firestore transaction)
  • Archived datasets excluded from count; only kind="people" counted
  • Error: PlanAccessError code="quota_exceeded", status 429

RBAC

  • Read: any org member (viewer+)
  • Create: operator+ (must set createdBy = auth.uid, name required)
  • Update: operator+  |  Archive: operator+
  • Delete: admin+ (hard delete + all items in chunks of 500)
  • Items: read = member; create = operator+; delete = operator+

Idempotent operations

  • Add item: returns { added: false } if already exists
  • Remove item: returns { removed: false } if doesn't exist
  • peopleCount incremented/decremented atomically
  • Cannot add people to archived dataset

Relationships & audit

  • Audience Rooms: pins → dataset items via createPeopleDatasetFromRoomPins()
  • Broadcasts: NOT currently used as broadcast targets (future: "dataset" target type)
  • Audit: people_dataset.created / updated / archived / deleted / item_added / item_removed
  • Resource type: "people_dataset"

API endpoints (Python FastAPI)

  • POST /v1/people-datasets/{company_id} — create dataset (201, quota-checked)
  • PUT /v1/people-datasets/{company_id}/{dataset_id} — update name/description
  • POST /v1/people-datasets/{company_id}/{dataset_id}/archive — archive (soft-delete)
  • DELETE /v1/people-datasets/{company_id}/{dataset_id} — hard delete + all items
  • POST /v1/people-datasets/{company_id}/{dataset_id}/items — add person (idempotent)
  • POST /v1/people-datasets/{company_id}/{dataset_id}/items/{external_id}/remove — remove person

All require authorize_org. Add item: external_id (1-500), source?, added_by, added_by_name, snapshot?.

Firestore shape

text
{envRoot}/datasets/{datasetId}
  |- name (1-200), description (max 2000)
  |- kind: 'people' (discriminator)
  |- createdBy, createdByName
  |- peopleCount: number (denormalized)
  |- traceCount: 0 (not used for people)
  |- retentionPolicy: { mode: 'inherit' } (not enforced)
  |- tags: [], archived: boolean
  +- createdAt, updatedAt: Timestamp

{envRoot}/datasets/{datasetId}/items/{externalId}
  |- externalId (1-500), source: audience_room | people_page
  |- addedBy, addedByName, addedAt
  +- snapshot: { externalId, channel, displayName?,
                  consentScope, turnCount, lastSeenAtMs?, firstSeenAtMs? }

Quota: maxDatasets (Free: 1 | Lite: 10 | Pro: 50 | Team: 50 | Enterprise: -1)
  Count excludes archived; only kind='people' counted

BroadcastsDashboard-only

Rylvo Broadcasts walkthrough

Outbound multi-channel messaging — send the same campaign to WhatsApp templates, SMS, email, and in-app from one composer. Audience uploads (CSV / XLSX / paste), platform-derived rate caps, per-recipient delivery tracking, and audit-grade history are built in. Manage from Dashboard → Broadcasts.

5-step composer

Audience → Channels → Content → Schedule → Review. Platform rules are surfaced at audience-pick time, never at delivery time.

  • 4 audience sources: saved segment, upload, paste, all past chatters
  • Toggleable channels with per-channel rule annotation
  • Template + LLM-enrichment + per-row variables
  • One-time, recurring (hourly → monthly), or "Send now"

12 broadcast channels

Email, in-app, WhatsApp, SMS (Twilio), WhatsApp (Twilio), Telegram, LINE, Slack, Discord, MS Teams, Messenger, Instagram, Web Widget. Each annotated with cold-send verdict and platform cap.

  • WhatsApp: approved-template picker synced from Meta Graph
  • WhatsApp (Twilio): rides Twilio Programmable Messaging with whatsapp: E.164 prefix
  • SMS: opt-in gated, Twilio REST direct from the dispatch route
  • LINE: reply/push via LINE Messaging API; channel secret + access token encrypted at rest
  • Telegram / Web Widget / Discord: cold-send blocked with explainer

Audience uploads

Server-parsed CSV / XLSX (SheetJS) and paste-list with classifier and dedupe. Up to 50k rows per upload; per-row variable columns merge into the template at send time.

  • Auto-detect identifier kind: phone, email, channel id
  • Warnings surfaced before commit
  • Optional "save to audience" mirror into endUsers

Delivery tracking

Every recipient writes a botBroadcastDeliveries row: queued / sent / failed / skipped, plus provider message ids where available.

  • Per-run doc with per-channel sent / failed / skipped counts
  • History drawer groups deliveries by channel + status
  • Audit log: broadcast.created / scheduled / deleted

Rate caps & safety

Token-bucket rate limiter per channel in the cron runner. Caps mirror documented platform ceilings so a hot run can't suspend the underlying account.

  • WhatsApp: 4,800/min (~80/s/number, Meta default)
  • SMS: 60/min (Twilio conservative)
  • Operators can lower per-broadcast, never raise above platform cap

Capabilities & RBAC

Four capability keys gate the feature, mirroring the rest of the dashboard.

  • broadcasts.view — viewer+
  • broadcasts.create — operator+
  • broadcasts.send — admin+
  • broadcasts.delete — admin+

API endpoints

  • POST /api/bots/broadcasts/dispatch — per-recipient × per-channel dispatch (cron + ID token)
  • POST /api/bots/broadcasts/generate-content — LLM enrichment (cron + ID token)
  • POST /api/broadcasts/upload — CSV / XLSX / paste-list parser
  • POST /api/broadcasts/recipients/save-to-audience — idempotent endUsers mirror
  • GET /api/channels/whatsapp/templates?orgId=…&botId=… — Meta-approved template catalog

Firestore shape

text
{envRoot}/botBroadcasts/{broadcastId}
  |- targets.type: "all" | "selected" | "segment" | "uploaded"
  |- targets.uploaded.rows: UploadedRecipient[]
  |- delivery.channels: BroadcastChannel[]
  |- delivery.whatsappTemplate: WhatsAppTemplateRef | null
  |- delivery.rateLimitPerMinute: number | null
  +- deliveries/{deliveryId}
      |- runId, channel, identifier, identifierKind
      |- status: "queued" | "sent" | "failed" | "skipped"
      |- providerMessageId, error
      +- attemptedAt, deliveredAt

{envRoot}/botBroadcastRuns/{runId}
  |- status: "success" | "partial" | "failed"
  |- perChannelStats: { [channel]: { sent, failed, skipped } }
  |- targetCount, sentCount, failedCount, skippedCount
  +- generatedContent, errors[]

Team & RBACDashboard-only

Rylvo Team and RBAC walkthrough

Role-based access control for every dashboard surface. Four ranked roles, email invitations, join requests, granular page-access grants, and ownership transfer. SSO / SAML and SCIM provisioning are available on Enterprise. Manage from Dashboard → Team.

Role hierarchy (ranked)

Four roles with strict rank-based enforcement. No action can target a member at or above your own rank.

  • Owner (rank 3) — full access, billing, transfer ownership
  • Admin (rank 2) — manage bots, keys, team, approvals, view audit log
  • Operator (rank 1) — run bots, tests, interventions, approve requests
  • Viewer (rank 0) — read-only dashboard access

Capability matrix (selected)

  • team.view — viewer+
  • team.invite — admin+
  • team.remove_member — admin+
  • team.change_role — admin+
  • team.transfer_ownership — owner
  • audit.view — admin+
  • audit.export — admin+ (+ Pro plan)
  • org.manage_sso — owner (+ Enterprise)

Invitations (email-based)

Admin+ invites by email. Plan limit enforced at invite and acceptance time, counting seats in use: agency delegates work on the agency's seats and are not counted against the client's limit. Deterministic invite ID from SHA256 hash. Rate limit: 10 invites/min.

  • Default expiry: 7 days. Status: pending / accepted / expired / cancelled
  • Atomic write: invitation + audit log + org usage counter in one transaction
  • Acceptance: email must match, plan limit re-checked, member doc created
  • Resend / cancel are admin+ actions, both audit-logged

Join requests (cross-org)

Non-members can request to join with a profile (role, workType, interests, useCase). Admins approve or deny with optional reason.

  • Profile captures: founder / engineer / PM / designer / ops / analyst / other
  • Work type: building agents, integrating API, managing workflows, analyzing data, exploring
  • Audit actions: page_access.approved / page_access.denied

Page access grants

Granular elevation for two gated pages: audit_log and billing. Non-admins see a "Request Access" button.

  • Request fans out in-app notification to all admins
  • Approve → pageId added to member's pageAccessGrants array
  • Deny → optional resolverNote. Revoke removes the grant
  • Access: role grants capability OR explicit pageAccessGrant

Role change & removal

Cannot change own role, cannot act on same/higher rank, cannot assign above own rank. Removal is soft-delete only.

  • Demotion enqueues durable outbox event for side-effects (API key revocation)
  • Removal: status → "deactivated", revokes all API keys via outbox
  • Ownership transfer: atomic swap, two audit entries, org.ownerId updated
  • SSO/SAML: available on Enterprise; domain ownership enforced, break-glass required, enforcement server-side
  • SCIM 2.0: user provisioning and deprovisioning; role writes refused, deprovision is a soft delete

API endpoints

  • POST /api/team/invite — create email invitation (admin+)
  • POST /api/team/invite/accept — accept invitation (any authenticated user)
  • POST /api/team/members/{uid}/role — change member role (admin+)
  • POST /api/team/transfer-ownership — transfer org ownership (owner only)

Firestore shape

text
organizations/{orgId}/members/{uid}
  |- uid, email, displayName
  |- role: owner / admin / operator / viewer
  |- status: active / deactivated
  |- pageAccessGrants?: ("audit_log" | "billing")[]
  |- joinedAt, deactivatedAt?, deactivatedBy?
  +- createdAt, updatedAt

organizations/{orgId}/invitations/{inviteId}
  |- email, role, status, invitedBy, inviterName
  |- expiresAt, acceptedAt?, emailStatus?

organizations/{orgId}/joinRequests/{requestId}
  |- userId, userEmail, orgId, status, profile, requestedRole?
  |- requestedAt, resolvedAt?, resolvedBy?, denialReason?

organizations/{orgId}/pageAccessRequests/{requestId}
  |- pageId, requesterId, requesterEmail, status
  |- requestedAt, resolvedAt?, resolvedBy?, resolverNote?

Agency & Client WorkspacesDashboard-only

Rylvo Agency and Client Workspaces walkthrough

Agencies can operate many client workspaces without turning the agency workspace into one shared tenant. Every client keeps its own owner, team, resources, billing, audit history, and Test/Production environments. The partnership grants approved access; it does not move the client's data. Manage it from Dashboard → Clients & Partners.

Available on Pro and Enterprise

Both the agency workspace and the client workspace need Pro or Enterprise before managed access can become active. An agency on Free or Lite cannot send a request. A client on those plans can still inspect and decline one, but must upgrade before approving. Viewing, declining, ending, and revoking remain available on every plan.

Free / Lite agency

View and close records; cannot send requests.

Free / Lite client

View and decline; cannot approve.

Pro / Enterprise on both

Full request, approval, delegation, switching, and revocation flow.

Why Test and Production show the same page

The agency–client relationship belongs to the whole workspace, so /dashboard/test/clients and /dashboard/production/clients intentionally resolve to one canonical Clients & Partners screen. This does not combine the client's environments. After a delegate opens the client workspace, the environment selector still determines whether they are working with Test or Production resources and runtime data.

1. Client shares its workspace ID

  • The client remains a completely separate Rylvo organization
  • The workspace ID identifies the destination; it grants no access by itself
  • The client owner, direct members, resources, and data do not move

2. Agency requests exact access

  • An agency admin selects the exact teammates to delegate
  • Each delegate receives admin, operator, or viewer access
  • The request can include a note and commercial relationship

3. Client owner approves

  • Nothing is granted while the request is pending
  • Only the client owner can approve or decline
  • Approval rechecks both plans, delegates, and direct-member collisions; it needs no free client seat

4. Delegates work inside the client

  • Open client uses the normal workspace switch and hard reload
  • Normal client RBAC, plan limits, and environment boundaries still apply
  • Every delegated membership records its agency and relationship provenance

Governance stays with the client

  • Delegated admins can build and deploy, but cannot govern the client workspace
  • Client team, ownership, organization settings, billing, and partnerships are blocked
  • Existing direct client memberships are never downgraded or reclassified

Billing and safe revocation

  • The client owns its subscription, wallet, transactions, usage, and entitlements
  • The commercial mode records the business arrangement; it does not weaken isolation
  • Ending the partnership removes delegated access and keys without deleting client data
  • Voluntary downgrades below Team are blocked until live relationships are closed
  • Forced entitlement loss auto-ends the link and preserves all client-owned data

Relationship lifecycle

text
pending_client_approval → active → ended
                        ↘ declined
declined / ended → pending_client_approval (new request revision)
Pro/Enterprise entitlement lost → ended + delegated access revoked

Dashboard API routes

  • GET /api/agency/portfolio?orgId=… — relationship summaries (current-org admin+)
  • POST /api/agency/relationships — request access (agency admin+ and Pro/Enterprise)
  • POST /api/agency/relationships/{id}/respond — approve (client owner and Pro/Enterprise) or decline (client owner on any plan)
  • DELETE /api/agency/relationships/{id} — end and revoke (agency admin+ or client owner)

Control-plane shape

text
agencyClientRelationships/{sha256(agencyOrgId:clientOrgId)}
  |- agencyOrgId, clientOrgId, relationshipId, revision
  |- status, delegates[], commercialModel, requestMessage?
  |- requestedBy/At, approvedBy/At?, declinedBy/At?, endedBy/At?
  +- server-owned: no direct browser reads or writes

organizations/{clientOrgId}/members/{delegateUid}
  |- membershipKind: "agency_delegate"
  |- agencyOrgId, agencyRelationshipId
  |- role: admin | operator | viewer
  +- delegatedAt, delegatedBy

Audit LogDashboard-only

Immutable, tamper-evident audit trail for every material action across the platform. 200+ action types, two-stage outbox write with HMAC-SHA256 integrity signatures, cursor pagination, and CSV export. Manage from Dashboard → Audit Log.

Two-stage write pattern

Every mutation stages an audit event into the outbox in the same Firestore transaction — atomicity is guaranteed. A Cloud Function drains the outbox into the canonical, read-only audit log.

  • Stage 1: auditOutbox (transactional, same batch as mutation)
  • Stage 2: auditLog (durable, read-only, drained by worker)
  • HMAC-SHA256 integrity signature on every outbox doc
  • Both web (Admin SDK) and Python API write through the same pipeline

Action catalog (200+)

Covers every material action: team, API keys, org, bots, workflows, prompts, tests, edge cases, guardrails, KB, traces, mission control, evolution, automation, MCP Hub, skills, audience, broadcasts, connectors, auth, and setup sessions.

  • 40+ resource types (member, bot, prompt, guardrail, trace, broadcast…)
  • Each entry: actor, action, resourceType, resourceId, details, IP, UA, source
  • Source discriminator: web / api / mcp / system

Filtering & search

Filter by action, resourceType, resourceId, source, date range, actor email, and actor name. Full-text search across all fields including details JSON.

  • Cursor-based pagination (opaque cursor: timestamp + docId)
  • Default 25/page, max 500/request
  • Rate limit: 120 req/min per IP on the BFF endpoint

Export & compliance

CSV export with all fields. Gated by capability and plan.

  • Columns: id, timestamp, actorName, actorEmail, actorId, action, resourceType, resourceId, details
  • Requires audit.export (admin+) AND Pro+ plan
  • Audit logs are immutable once canonicalized
  • Retention follows org-specific policy; GDPR purge via observability endpoints

RBAC

View and export are separately gated. Non-admins can request access via the page-access grant system.

  • audit.view — admin+ OR pageAccessGrants: ["audit_log"]
  • audit.export — admin+ AND Pro+ plan
  • Non-admins see "Request Access" on the audit-log page

Integrity & security

HMAC-SHA256 signature on every outbox doc prevents tampering. Firestore rules block direct writes to auditLog.

  • Signed fields: idempotencyKey, action, resourceType, resourceId, actor, details, source
  • Canonical JSON with sorted keys
  • No direct client writes to auditLog — all go through outbox staging
  • Reader handles both nested actor objects and flat legacy fields

API endpoint (BFF)

  • GET /api/audit?orgId=…&action=…&resourceType=…&startDate=…&endDate=…&q=…&actorEmail=…&actorName=…&cursor=…&limit=… — list audit entries

Auth: Firebase ID token (Bearer). Response: { orgId, total, records[], nextCursor }. Cache-Control: private, max-age=5, stale-while-revalidate=30.

Firestore shape

text
organizations/{orgId}/auditOutbox/{docId}   (transient staging)
  |- idempotencyKey, action, resourceType, resourceId
  |- actor: { uid, email, displayName }
  |- details: { schemaVersion, ...custom }
  |- ipAddress, userAgent, source
  |- processed: boolean, processedAt, error, retryCount
  |- stagedAt, stagedBy, integrity (HMAC-SHA256)

organizations/{orgId}/auditLog/{docId}   (canonical, read-only)
  |- idempotencyKey, action, resourceType, resourceId
  |- actor: { uid, email, displayName }
  |- details: { schemaVersion, ...custom }
  |- ipAddress, userAgent, source
  +- timestamp: Timestamp

Indexes required:
  auditOutbox: (processed, stagedAt)
  auditLog: (action, timestamp desc), (resourceType, timestamp desc),
            (source, timestamp desc), (timestamp desc, __name__ desc)

API KeysDashboard-only

Org-level API key generation, management, and revocation with environment scoping, permission scopes, rate limit overrides, and SHA-256 hashing. Keys authenticate external API requests (bot-run, KB query, KB documents). Manage from Dashboard → API Keys.

API key document

  • name — human-readable label
  • keyPrefix — first 12 chars for display
  • keyHash — SHA-256 of raw key (never store raw)
  • environment — production | test (legacy staging/development map to test)
  • permissions — API scopes array
  • rateLimitOverride — calls/min (null = default 60)
  • botId — optional bot binding

Key generation & hashing

  • Format: rylv_{40 hex chars} (46 chars)
  • Entropy: 20 bytes = 160 bits (crypto.getRandomValues)
  • Hashing: SHA-256 via Web Crypto API
  • Only hash persisted; raw key shown once at creation
  • Prefix (first 12 chars) stored for display identification

Permission scopes (5 + 2 wildcards)

  • respond — POST /api/v1/bot-run + widget-poll
  • kb:read — POST /api/v1/kb/query
  • kb:write — POST /api/v1/kb/documents
  • tests:read — POST /api/v1/tests/gate
  • trace.read — reserved for future
  • * — full access  |  kb:* — all KB
  • Normalization drops unrecognized scopes; defaults to ["respond"]

Authentication flow

  • Headers: Authorization: Bearer | X-API-Key | X-Rylvo-Key
  • SHA-256 hash raw key, collection-group query by keyHash
  • Verify status == "active", extract orgId from path
  • Async update lastUsedAt (non-fatal if fails)
  • Returns AuthIdentity with permissions + rateLimitOverride

CRUD operations

  • create: generate + hash + normalize, batch write (doc + counter + audit)
  • list: filter by status, ordered by createdAt desc
  • update: name, environment, permissions, botId + audit
  • revoke: status → "revoked", decrement counter + audit
  • revokeAllForMember: bulk revoke on member removal
  • hardDelete: physical deletion (provisioning rollback)

Rate limiting

  • Default: 60 calls/minute per key
  • Override via rateLimitOverride field
  • Token bucket via consumeTokenUnified()
  • 429 response with Retry-After header
  • Per-route buckets (bot-run, kb/query, etc.)

RBAC & plan limits

  • apiKeys.view — admin+
  • apiKeys.create — admin+
  • apiKeys.revoke — admin+
  • Free: 0 | Starter: 0 | Lite: 10 | Pro: 25 | Team: 25 | Enterprise: unlimited
  • A key a Free workspace already holds keeps working; only creating a new one is refused
  • Quota enforced in Firestore rules (withinApiKeyQuota)
  • Non-admins see RestrictedPlaceholder

Audit & cleanup

  • Events: api_key.created / updated / revoked (with actor, IP, UA)
  • Bulk revoke logs { count, memberUid }
  • Bot deletion: hard-deletes bound keys, decrements active count only
  • Workspace Architect: auto-creates key with default permissions, rollback via hardDelete
  • Migration route: normalizeApiKeyPermissions backfill (idempotent)

API endpoints (BFF)

  • POST /api/v1/bot-run — execute bot turn (permission: respond, idempotency replay)
  • POST /api/v1/kb/query — query KB (permission: kb:read)
  • POST /api/v1/kb/documents — upload documents (permission: kb:write, max 100)
  • POST /api/v1/tests/gate — release gate for CI (permission: tests:read)

Auth: API key via Bearer / X-API-Key / X-Rylvo-Key header. Rate limited per key.

Firestore shape

text
{envRoot}/apiKeys/{keyId}
  |- name, keyPrefix (12 chars), keyHash (SHA-256)
  |- environment: production | test  (legacy: staging, development map to test)
  |- permissions: string[] (respond, kb:read, kb:write, trace.read, *, kb:*)
  |- status: active | revoked
  |- rateLimitOverride: number | null (calls/min)
  |- botId?, botName?
  |- createdBy, createdAt
  |- lastUsedAt?, revokedAt?, revokedBy?

Quota: usage.apiKeyCount vs settings.maxApiKeys
  Free: 1 | Lite: 10 | Pro: 25 | Team: 25 | Enterprise: -1 (unlimited)

ApprovalsDashboard-only

Human-in-the-loop approval workflow for tool execution. Two systems: connector tool approvals (API-level, for webhook/connector tools with requires_approval flag) and MCP tool approvals (MCP Hub, for tools with require_approval permission). Mission Control adds urgency-based timeouts and risk scoring. State machine: pending → approved / denied / expired.

Connector tool approval

  • Triggered when tool has safety_restrictions.requires_approval = True
  • approval_id format: aprv_{tool_call_id}
  • Default TTL: 24 hours (86400s)
  • Fire-and-forget: tool returns failure with approval_id immediately
  • Bot/frontend polls status separately; no auto re-invoke on approval

MCP tool approval

  • Permission levels: allow | require_approval | deny
  • Risk-based defaults: low/medium → allow, high/critical → require_approval
  • Default TTL: 10 minutes (600,000 ms)
  • argsPreview (redacted JSON, max ~2KB) + optional llmRationale
  • Real-time Firestore subscriptions for pending approvals

State machine

  • pending → approved (via service.approve)
  • pending → denied (via service.deny with reason)
  • pending → expired (auto-expiry when TTL elapses)
  • Only pending → approved/denied allowed (idempotent)
  • Double-approve/deny returns None (already resolved)
  • Mission Control adds: auto_denied status

Mission Control urgency levels

  • immediate — 15s timeout, low-risk informational
  • standard — 120s timeout, data access / PII-related
  • extended — 900s timeout, financial / legal / compliance
  • Expiry computed server-side (createdAt + timeoutSeconds)
  • Includes riskScoreAtTime + riskSignals[] + conversationContext

ApprovalService methods

  • request_approval() — create pending, persist, log event
  • approve() — verify pending, update, log, idempotent
  • deny() — verify pending, update + reason, log, idempotent
  • list_pending() — query + filter expired + log stale

Expiration & cloud functions

  • cleanupExpiredMcpApprovals — every 5 min, MCP approvals past TTL
  • missionControlExpireApprovals — every 2 min, MC approvals past timeout
  • Both write audit entries for system-initiated expiry
  • Frontend polling: 1-30s exponential backoff, 5-min local timeout

RBAC

  • Connector approvals: read = admin+; write = server-only (rules: false)
  • MC approvals: read = member; create/update = operator+; delete = admin+
  • MCP approvals: read = member; create/update = operator+; delete = admin+
  • resolve_approval capability: operator+ (Mission Control)
  • MCP Hub Approvals page: admin+ for approve/deny buttons

Audit & analytics

  • MC expiry writes audit: eventType "approval_resolved", actor "system_sweep"
  • Analytics: MCApprovalResolvedEvent { request_id, bot_id, action_type, resolution, response_time_ms, urgency }
  • Audit log: approval.requested / approved / denied / expired / resolved
  • Idempotency key on MC approvals for exactly-once semantics

API endpoints (Python FastAPI — connector approvals)

  • POST /v1/approvals/{approval_id}/approve — approve (query: approver_email?)
  • POST /v1/approvals/{approval_id}/deny — deny (query: reason?, approver_email?)
  • GET /v1/approvals/pending?company_id=…&bot_id=…&session_id=… — list pending

GET pending requires authorize_org. 404 if not found or already resolved. MCP approvals managed via dashboard at /dashboard/mcp-hub/approvals.

Firestore shape

text
{envRoot}/approvalRequests/{approvalId}   (connector + Mission Control)
  |- approval_id (aprv_{tool_call_id}), tool_call_id, tool_name
  |- arguments: dict, status: pending | approved | denied | expired
  |- company_id, org_id, bot_id?, session_id, workflow_id?
  |- requested_at, expires_at (24h default)
  |- resolved_at?, resolved_by?, denial_reason?
  |- trace_id, request_id
  |- (MC) urgency, timeoutSeconds, riskScoreAtTime, riskSignals[]
  |- (MC) conversationContext, idempotencyKey?
  +- created_at: Timestamp

{envRoot}/mcpApprovalRequests/{requestId}   (MCP Hub)
  |- requestId, serverId, serverName, toolId, toolName, toolDisplayName
  |- botId, botName, conversationId?, requestedBy, requestedByDisplay?
  |- argsPreview (redacted, ~2KB), llmRationale?
  |- status: pending | approved | denied | expired
  |- reviewedBy?, reviewedByDisplay?, decisionReason?
  |- expiresAt (10 min default), reviewedAt?
  +- createdAt, updatedAt: Timestamp

NotificationsDashboard-only

Rylvo Notifications walkthrough

Unified notification system with 60+ types across 8 categories. In-app notifications delivered via real-time Firestore subscriptions; email via Resend; webhook dispatch to Slack/Discord/Teams. Unified inbox hook merges mentions + in-app alerts across all orgs. Manage from Dashboard → Notifications.

In-app notification document

  • userId (recipient), orgId, type
  • title, message, link?
  • data?: Firestore primitives (string | number | boolean)
  • read: boolean, readAt: Timestamp | null

Trace comment mention document

  • recipientUid, type (trace_mention | comment_reply | room_mention | pin_mention)
  • payload: commentId, entityType, entityId, entityLabel, actorUid, actorName, snippet (~120 chars)
  • Optional: parentRoomId, traceId, traceSource, traceLabel

77 types across 8 categories

  • billing (19): credits_low, quota_exceeded, subscription_*, billing_addon_*, referral_reward
  • mission_control (20): alert_rule_triggered, channel_error, byok_unhealthy, approval_requested, bot_budget_*
  • team (15): join_request, member_joined, page_access_*, member_role_changed
  • system (12): welcome, onboarding_step, kb_indexing_*, bot_deleted, bot_import_completed, bot_import_needs_attention, timezone_unset
  • retention (4): retention_warning, retention_deleted, dormancy_*
  • edge_cases (4): edge_case_alert, guardrail_triggered, red_team_run_complete, fix_proposal_applied
  • broadcasts (2): broadcast, broadcast_failed · evolution (1): evolution_alert

Delivery channels (4)

  • In-app: real-time Firestore subscriptions (<100ms)
  • Email: Resend (mention emails, welcome; per-member opt-in)
  • Webhooks: Slack, Discord, Teams, generic (2 retries, 500ms backoff, 8s timeout)
  • Bot tools: send_notification__{slug}__{id} runtime tools (best-effort)

Real-time unified inbox

  • useNotificationInbox(): merges mentions + in-app, scoped to the active org
  • Parallel per-org Firestore subscriptions; other workspaces surface as unread counts + switch affordance
  • Exposes: items[], unreadCount, otherOrgUnread[], loading, markItemRead, markAllRead, deleteItems
  • Max 100 per org per subscription

Lifecycle & triggers

  • Created (read=false) → Mark Read → Delete (hard-delete)
  • Mark all read: capped at 200 docs per call
  • Triggers: guardrail escalations, degraded attachments, observability alerts, join requests, broadcasts, mentions, welcome
  • Engine triggers are fire-and-forget post-hooks (never block bot response)

Notification channels (Mission Control)

  • Types: slack, email, webhook, discord, teams
  • config: webhookUrl?, emails?, headers?
  • events[]: which alert events trigger this channel
  • botIds?: optional bot linking
  • lastUsedAt, lastStatus (success | failed)

RBAC & preferences

  • In-app: read = userId == auth.uid; update = only read/readAt fields; delete = false (via API)
  • Mentions: read = orgMember + recipientUid match; create = actorUid == auth.uid
  • Channels: read = member; create/update/delete = admin+
  • Dispatch API: operator+ required
  • Preferences: notificationPrefs.mentionEmail (default true)

API endpoints

  • POST /api/notifications/create — multi-recipient fan-out (30/min per org, max 500 recipients)
  • POST /api/notifications/delete — permanent deletion (max 100 items, ownership verified)
  • POST /api/notifications/dispatch — Mission Control webhook dispatch (10/min per org, operator+)
  • POST /api/notifications/email — mention emails via Resend
  • POST /api/notifications/welcome — one-time welcome + email (transactional guard)

Create: type (registry-validated), title, message, link?, data?, audience (all_members|admins|{userIds}). Dispatch: channels[], alert{ruleName, severity, trigger, message, botName, conversationId, details}.

Firestore shape

text
organizations/{orgId}/inAppNotifications/{notificationId}
  |- userId, orgId, type (from registry)
  |- title, message, link?, data?
  |- read: boolean, readAt: Timestamp | null
  +- createdAt: Timestamp

organizations/{orgId}/notifications/{notifId}  (trace comment mentions)
  |- recipientUid, type (trace_mention | comment_reply | room_mention | pin_mention)
  |- read: boolean
  +- payload: { commentId, entityType, entityId, entityLabel,
                actorUid, actorName, snippet, parentRoomId?, traceId? }

{envRoot}/notificationChannels/{channelId}  (Mission Control)
  |- name, type (slack|email|webhook|discord|teams), enabled
  |- config: { webhookUrl?, emails?, headers? }
  |- events[], botIds?, lastUsedAt, lastStatus
  +- createdBy, createdAt, updatedAt

organizations/{orgId}/members/{uid}.notificationPrefs:
  { mentionEmail: boolean (default true) }

Ready to integrate?

Create an account, generate an API key, and start making decisions.