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
Video coming soon
Click to preview (placeholder)
Make your first API call in under a minute. You need a botId and an API key from the dashboard.
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
Video coming soon
Click to preview (placeholder)
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.
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)/health,/ready,/live,/flags(unauthenticated)
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- 128 tools for bots, prompts, KB, skills, connectors
- OpenAPI 3.1 spec at
/openapi.json
Manage from app.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.
Authentication
Video coming soon
Click to preview (placeholder)
All API requests require an API key sent in the X-API-Key header. Create keys from your dashboard.
X-API-Key: rylv_abc123...Security: Never expose API keys in client-side code. Always call the API from your backend.
Provider Keys (BYOK)Dashboard-only
Video coming soon
Click to preview (placeholder)
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.
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
- Auto (default) - the model's native provider key when added (direct to e.g.
api.anthropic.com), otherwise your OpenRouter key. - Always OpenRouter - every chat runs on your OpenRouter key, even when a direct key exists.
- Native keys only - OpenRouter is never used; models without a direct key are locked.
- 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
- OpenAI -
openai/* - Anthropic -
anthropic/*(native Messages API adapter) - Google Gemini -
google/*(nativegenerateContentadapter) - DeepSeek -
deepseek/* - xAI (Grok) -
x-ai/* - Kimi / Qwen / GLM -
kimi/*,qwen/*,zhipu/* - Mistral -
mistralai/* - Groq -
groq/*(needs its own key; not on OpenRouter)
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.
Test before you ship
The Settings page has a Test button next to every saved key. It sends a 4-token ping (< $0.001) to the provider and reports auth / rate-limit / network errors inline, plus round-trip latency. Use Test all to re-verify every configured key at once - ideal after a key rotation.
Bot Tagging (Mandatory)
Video coming soon
Click to preview (placeholder)
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
Create a bot
Go to Dashboard → Bots → New Bot. Give it a name and description.
Copy the bot ID
Each bot gets a unique ID. Copy it from the bot card.
Tag every API request
Include "botId": "your_bot_id" in the request body.
View per-bot analytics
Filter traces, analytics, and billing by bot.
Example request with botId
{
"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
Video coming soon
Click to preview (placeholder)
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
Video coming soon
Click to preview (placeholder)
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
- Assertions on output text (contains, regex, semantic similarity)
- 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
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
Video coming soon
Click to preview (placeholder)
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.
Endpoints
Video coming soon
Click to preview (placeholder)
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/bot-run | Run one bot or agent-group turn (scope: respond) |
| POST | /api/v1/kb/query | Query a knowledge-base connection (scope: kb:read) |
| POST | /api/v1/kb/documents | Upload documents to a KB source (scope: kb:write) |
| GET | /health | Liveness (process alive) |
| GET | /ready | Readiness (can handle requests) |
| GET | /live | Uptime probe |
| GET | /flags | Rollout 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)
Video coming soon
Click to preview (placeholder)
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
Video coming soon
Click to preview (placeholder)
{
"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
| Field | Type | Description |
|---|---|---|
| botId | string | Bot identifier (from Dashboard → Bots). Either botId or groupId is required. |
| message | string | The end user's current message |
Optional Fields
| Field | Type | Description |
|---|---|---|
| groupId | string | Agent-group id (run a multi-bot team instead of a single bot) |
| conversationHistory | object[] | Prior turns: { role: 'user' | 'assistant', content } |
| sessionId | string | Conversation/session id; echoed back as conversationId |
| endUserExternalId | string | Stable end-user id in your namespace (≤256). Required for display name / email. |
| endUserDisplayName | string | Human-readable name shown in Traces (≤200) |
| endUserEmail | string | End-user email for support handoff (validated) |
| channel | string | api (default) | widget | whatsapp | slack | unknown |
| model | string | Override model (e.g. openai/gpt-4.1-mini, anthropic/claude-sonnet-4.6) |
| maxTokens | number | Max output tokens for this turn |
| temperature | number | Sampling temperature for this turn |
| disableTools | boolean | Disable tool/connector calls for this turn |
| maxToolIterations | number | Cap the tool-call loop iterations |
| responseFormat | object | Structured output: {type:"json_object"} or {type:"json_schema", json_schema:{...}} — constrains the final reply (OpenAI-compatible models only) |
| stream | boolean | Token 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
Video coming soon
Click to preview (placeholder)
{
"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,
"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
| Field | Description |
|---|---|
| response | The assistant's reply text to show the user |
| runId | Unique id for this turn — find it in Dashboard → Traces |
| conversationId | Echoes the request sessionId |
| model | The model that actually served the turn |
| latencyMs | Server-side turn latency (ms) |
| tokens | { prompt, completion, total } token counts |
| guardrails | { inputBlocked, outputRewritten, blockedBy, escalated, escalatedBy } |
| toolCalls | Tools invoked this turn (name, disposition, latency, cost) |
| sessionEnd | Set when the bot ended the session ({ reason, summary? }) |
| kb | KB grounding telemetry incl. citations (when retrieval ran) |
| pendingApproval | Present when a tool call awaits operator approval |
Flows
Video coming soon
Click to preview (placeholder)
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 instructioncollectData fields to slot-fill before advancingallowed_toolsTool scope for the step (all / none / a list)transitionsEdges to the next step, evaluated every turnTransition kinds
alwaysFires unconditionallydata_completeAll of the step's collect fields are gatheredintentThe classified intent matchesconditionA DSL expression over the turn context is truetool_resultA named tool ran successfully this turnError Handling
Video coming soon
Click to preview (placeholder)
Invalid Request
Malformed JSON, missing message, missing botId/groupId, or a bad field shape.
Authentication Error
Missing or invalid API key.
Forbidden
Key lacks the scope, is bot-bound to a different bot, or the org is suspended.
Not Found
Bot, group, or resource not found.
Rate Limited
Per-key rate limit exceeded — see the Retry-After header.
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.
// Error response format
{
"error": "One of `botId` or `groupId` is required.",
"code": "invalid_request"
}Multi-Turn Example
Video coming soon
Click to preview (placeholder)
Keep a stable sessionId and pass the running transcript in conversationHistory so the bot has context.
Turn 1 - User reports issue
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)
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
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
Video coming soon
Click to preview (placeholder)
/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
Video coming soon
Click to preview (placeholder)
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.
| Plan | Monthly | Requests / Month |
|---|---|---|
| Free | $0 | 2,500 |
| Lite | $40 | 100,000 |
| Pro | $100 | 500,000 |
| Team | $400 | 500,000 |
| Enterprise | Custom | Custom |
Rate-limit headers on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After on 429.
Billing & Usage
Video coming soon
Click to preview (placeholder)
Rylvo's billing model is simple: a monthly subscription plan covers platform capacity (bots, prompts, guardrails, KB sources, connectors, observability, SLA), 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, embeddings — all run on your own provider key with zero markup. No Credits, no wallet, no monthly grant.
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
| Plan | Price | Headline limits | Notes |
|---|---|---|---|
| Free | $0 | 1 bot · 7-day retention | BYOK bot chat + AI features, all free |
| Lite | $40 | 3 bots · 14-day retention · 100k req/mo | Solo builder toolkit; no Advanced AI suite or Broadcasts |
| Pro | $100 | 25 bots · 30-day retention · 500k req/mo | Advanced AI suite + Broadcasts |
| Team | $400 | 25 bots · 90-day retention · 500k req/mo | Pro + you + 2 seats included ($40/extra seat) + dedicated support |
| Enterprise | Custom | Negotiated | SSO, multi-region, priority routing |
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:
trace.cost_usd = (input_tokens × model.price_per_input_token)
+ (output_tokens × model.price_per_output_token)Response headers on /api/v1/bot-run
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | The key's per-minute request capacity |
| X-RateLimit-Remaining | Requests left in the current rate-limit window |
| X-RateLimit-Reset | Unix seconds when the window resets |
| Retry-After | Seconds to wait before retrying (on 429) |
| Idempotent-Replay | true 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 downgradedrequireQuota— usage ≥ plan caprequireProviderKey— AI feature needs your own LLM key (BYOK, free)requireStorageEventsBudget— storage pool exhausted- Returns
PlanAccessErrorwithupgradeTarget
Durable outbox architecture
- API routes publish to a durable
billingOutboxbefore Pub/Sub - Cloud Function
billingOutboxWorkeris the only account writer - Atomic transactions with idempotency + negative clamp
- Daily
billingReconciliationruns 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
Video coming soon
Click to preview (placeholder)
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)
| Method | Path | Description |
|---|---|---|
| GET | /v1/traces/{trace_id}/replay | Full replay payload for one trace (prompt stack, guardrails, tool calls, per-stage timings) |
| GET | /v1/sessions/{session_id}/traces?limit=50 | List every run in a session, newest first |
| GET | /v1/analytics/companies/{company_id}/overview?limit=500&top_n=10 | Escalation rate, avg latency, and token/cost rollups over the window |
| GET | /v1/analytics/companies/{company_id}/runs?limit=100 | Recent 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-rowsPostgres aggregates - Billing:
sum_cost_by_bot(per-bot cap) - Audience / GDPR: canonical customer rows
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
curl https://rylvo.com/api/v1/traces/tr_def456/replay \
-H "X-API-Key: YOUR_API_KEY"AnalyticsDashboard-only
Video coming soon
Click to preview (placeholder)
Analytics are dashboard-driven and backed by the canonical observability store (Postgres), not raw Firestore scans. Manage from Dashboard → Analytics.
Analytics Rooms
Customizable dashboards with a widget grid. Create, save, and share rooms.
- Default room auto-seeded on first visit
- Bot/org-scoped date range per room
- Viewer+ read, operator+ create/edit
- Share via deep link
Widgets
Build from a catalog or pick from templates. Each widget has its own date range, bot scope, and visualization.
- Request volume, token breakdown, latency
- Model usage, cost, verification health
- Escalation rate, recent traces, cost by bot/model/conversation
- Template gallery for common dashboards
Data sources
Aggregates from the canonical trace store and the billing ledger.
- Canonical traces via
analytics-rows - Internal (dashboard / MCP):
GET /v1/analytics/companies/{id}/overview - Internal (dashboard / MCP):
GET /v1/analytics/companies/{id}/runs - Transactions feed for cost widgets
Date ranges
Preset or custom windows.
- 1h, 24h, 7d, 30d, 90d, all time
- Custom start/end day boundaries
- Per-widget range overrides
Plan gating
Advanced Analytics is a Lite+ feature. Free orgs see a feature upgrade prompt; creating custom rooms and editing widgets requires operator+ on Lite or above.
Analytics RoomsDashboard-only
Named, customizable report dashboards with draggable chart cards, per-room filters (bot, env, date range), and collaborative sharing across the org. Powered by a Langfuse-style widget engine with 4 data sources, 19 metrics, 11 breakdowns, and 7 chart types. Manage from Dashboard → Analytics.
Room document
name(1-200 chars),createdBy(UID)filters: botId (string | "all"), env (all|test|production), dateRange (1h|24h|7d|30d|90d|all)charts: array of AnalyticsRoomChartConfig- Real-time Firestore listener for room list updates
Chart card config
type: 17 legacy types +custom_widgetposition(0-indexed),title?overridewidth: half | full |height: compact | normal | tallcolor: 8 colors (blue, emerald, amber, red, purple, pink, cyan, neutral)
Custom widget engine (Langfuse-style)
- 4 views: traces, tool_calls, cost, runs
- 19 metrics: count, unique_users, total_cost, p95_cost, total_tokens, avg_latency, pass_rate, error_rate, etc.
- 11 breakdowns: none, bot, model, env, status, user, channel, source, day, hour, week
- 7 chart types: line, bar_v, bar_h, big_number, pie, histogram, pivot
- Filters: 9 columns, 7 operators (=, !=, in, not_in, contains, starts_with, >, <)
topN: limit for breakdown charts (default 10)
Default room seed (7 widgets)
- Total Traces (big_number, blue) + Total Cost (big_number, emerald)
- P95 Latency (big_number, amber) + Error Rate (big_number, red)
- Traces Over Time (line, blue, breakdown: day)
- Cost by Model (bar_h, emerald, breakdown: model, topN: 8)
- Verification Distribution (pie, emerald, breakdown: status)
Dashboard templates (4 categories)
- cost — spend tracking, token economics, billing (9 widgets)
- performance — latency, throughput, bot + tool perf
- usage — volume, users, channels, conversations
- quality — verification pass/fail, errors, guardrails
Widget rendering pipeline
- 1. Load data (traces, runs, transactions, bots)
- 2. resolveWidget() converts legacy types to WidgetConfig
- 3. applyRoomScope() injects room's botId filter
- 4. WidgetRenderer dispatches to chart component
- 5. computeWidget() / computeHistogram() aggregates
- Legacy types with no equivalent render "retired" card
RBAC & plan limits
- Read: any org member (viewer+) — rooms are org-wide
- Create/Update/Delete: admin+ only
- Feature:
advancedAnalytics— Lite+ required - Free: feature upgrade prompt; analytics rooms need Lite+
- No per-room count quota — unlimited within plan tier
Dashboard UI features
- Room sidebar (real-time, rename/delete menu, + New Room)
- Drag-and-drop chart reordering (position updated on drop)
- Room customizer modal (name, filters, chart management)
- Widget builder + templates + saved widget library
- Date range override (per-user, localStorage)
- Responsive: 2 columns desktop, 1 column mobile
API endpoints (Python FastAPI)
- GET /v1/analytics-rooms/{company_id} — list all rooms (sorted by updatedAt desc)
- GET /v1/analytics-rooms/{company_id}/{room_id} — get single (404 if not found)
- POST /v1/analytics-rooms/{company_id} — create (201, { room_id, ok })
- PUT /v1/analytics-rooms/{company_id}/{room_id} — update (partial)
- DELETE /v1/analytics-rooms/{company_id}/{room_id} — delete (idempotent)
All require authorize_org. Create: name (1-200), filters?, charts?, created_by?. Events: analytics_room_created / updated / deleted.
Firestore shape
organizations/{orgId}/analyticsRooms/{roomId}
|- id, name (1-200), createdBy
|- filters: { botId, env, dateRange }
|- charts: [{ id, type, position, title?,
| width?, height?, color?,
| customWidget?, savedWidgetId? }]
+- createdAt, updatedAt: Timestamp
WidgetConfig (inline in customWidget):
view: traces | tool_calls | cost | runs
metric: count | total_cost | p95_latency | error_rate | ... (19 total)
breakdown: none | bot | model | env | status | day | hour | week | ...
chartType: line | bar_v | bar_h | big_number | pie | histogram | pivot
filters: [{ id, column, op, value }]
topN?: number (default 10)SDKs & Libraries
Video coming soon
Click to preview (placeholder)
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
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
Video coming soon
Click to preview (placeholder)
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
Choose an agent category
Select what type of agent this prompt controls (e.g., Response Composer, Verifier).
Write your prompt content
Use {{placeholder_key}} syntax for dynamic values. Templates are provided for each category.
Add placeholders
Define keys, labels, default values, and whether they're required. Use the template library for common ones.
Assign to a bot (optional)
Link the prompt to a specific bot, or leave it shared. Tag requests with prompt_id in the API.
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.
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
| Key | Default | Description |
|---|---|---|
| {{company_name}} | Acme Corp | The customer's company name |
| {{user_name}} | John | The end user's display name |
| {{product_name}} | Widget Pro | The product being discussed |
| {{industry}} | Technology | The customer's industry vertical |
| {{tone}} | professional | Response tone (formal, casual, empathetic) |
| {{language}} | English | Response language |
| {{max_length}} | 256 | Maximum 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
Video coming soon
Click to preview (placeholder)
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.
Edit sources
humanManual edit by a teammate from the dashboard
architectApplied via Workspace Architect (conversational setup / edit mode)
auto_generatedProduced by the Self-Improving optimizer or Agent Evolution rule generator
fix_proposalAccepted from an Edge Case Engine fix proposal
systemInternal migration or default seed
red_teamCreated in response to a red-team finding
What's versioned
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
Video coming soon
Click to preview (placeholder)
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
The system gathers performance metrics from real API requests that use this prompt - success rate, verification pass rate, user satisfaction signals, and latency.
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.
Each variant is scored against your chosen metric. Scoring uses structural analysis, constraint quality, and historical performance patterns.
The best-performing variant is identified. It must beat the current prompt by at least the improvement threshold (default 2%) to qualify.
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
| Option | Default | Description |
|---|---|---|
| Strategy | OPRO | Which optimization algorithm to use |
| Evaluation Metric | Success Rate | success_rate | verification_pass | user_satisfaction | latency |
| Run Frequency | Daily | hourly | daily | weekly | manual |
| Auto-Promote | Off | Automatically promote winning variants to active |
| Improvement Threshold | 2% | Minimum improvement required to qualify as a winner |
| Max Variants Per Run | 5 | Number of candidate variants generated per optimization run |
| Min Sample Size | 50 | Minimum API requests before first optimization run |
Enabling Self-Improvement
Open the prompt detail panel
Click on any prompt from the Prompts page to view its details.
Expand 'Self-Improving Prompts'
Click the panel header to expand the optimization settings.
Toggle 'Enable Auto-Improvement'
This activates the optimization loop for this prompt.
Choose your strategy
Select OPRO, APE, DSPy Bootstrap, or PromptBreeder depending on your needs.
Set your metric and frequency
Choose what to optimize for and how often to run.
Decide on auto-promote
Turn on to automatically deploy winning variants, or leave off for manual review.
Prompt API Usage
Video coming soon
Click to preview (placeholder)
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
Resolve
The bot's assigned prompts load their active versions.
Substitute
All {{placeholder}} values are filled from bot/org defaults.
Inject
The resolved text becomes part of the system prompt for the turn.
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
Video coming soon
Click to preview (placeholder)
The Edge Case Engine is a closed-loop system that monitors your bot traces, automatically discovers failure patterns, classifies them into 12 categories, and suggests fixes. It uses a 5-stage pipeline: Observe → Detect → Classify → Fix → Verify. Manage from Dashboard → Edge Cases. Pro+ plan feature.
3 detection mechanisms
- Passive rule-based — scans trace signals: verification_failed, missed_escalation_signal, execution_blocked, has_errors, no_evidence, safety_override
- LLM-based scan —
POST /v1/ai/{org}/edge-case-scansends trace summaries to an LLM (default openai/gpt-4.1-mini). 10 scans/min per org, runs free on your own LLM key (BYOK) - 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 → resolvedAlternative: dismissed (false positive) or deleted (permanent removal).
Fix proposals
6 fix types with confidence scores (0.0–1.0) and impact ratings:
- guardrail_add — auto-generate a new guardrail
- prompt_edit — proposed before/after prompt text
- threshold_adjust — change a parameter value
- escalation_rule — add or modify escalation logic
- workflow_expand — extend workflow coverage
- custom — manual steps
Lifecycle: proposed → applied / rejected / reverted
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
organizations/{orgId}/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
organizations/{orgId}/fixProposals/{proposalId}
|- edgeCaseId, fixType, title, description
|- impact, confidence, status
|- details: { guardrailConfig?, promptChanges?, thresholdChanges?, customSteps? }
|- regressionTestIds[]
+- appliedBy, appliedAt, createdBy, createdAt
organizations/{orgId}/redTeamRuns/{runId}
|- name, botId, strategies[], status
|- config: { testsPerStrategy, maxConcurrent, timeoutMs, ... }
|- results: { totalTests, passedTests, failedTests, edgeCasesFound }
+- edgeCaseIds[], startedAt, completedAt
organizations/{orgId}/redTeamRuns/{runId}/results/{resultId}
|- strategy, testInput, botResponse, passed
|- failureCategory, failureReason, severity, edgeCaseId
|- guardrailResults[], toolCalls[], traceId, latencyMs
+- model, tokens, createdAtGuardrails Dashboard-only
Programmable safety rules that run on every bot response
Video coming soon
Click to preview (placeholder)
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.
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
Video coming soon
Click to preview (placeholder)
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
Video coming soon
Click to preview (placeholder)
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. 4 tabs: Overview, Scheduled Tasks, Reports, Settings.
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
organizations/{orgId}/scheduledTasks/{taskId}
|- name, type, cadence, enabled, botId
|- config: { traceLimit?, redTeamStrategies?, aiConfig?, scheduleConfig? }
|- notificationChannelIds[], lastRunAt, lastRunStatus, nextRunAt
+- runCount, failCount
organizations/{orgId}/notificationChannels/{channelId}
|- name, type, enabled, config, events[], botIds[], scope
+- lastUsedAt, lastStatus
organizations/{orgId}/reportConfigs/{configId}
|- name, cadence, sections[], dataSources[], filters
|- recipientFilter, customRecipientEmails[], notificationChannelIds[]
|- aiConfig?, outputFormats[], scheduleConfig?
+- lastGeneratedAt, nextGenerateAt
organizations/{orgId}/reports/{reportId}
|- configId, status, sections[], data, distributedTo[]
|- llmFormattedContent?, pdfUrl?, tokenUsage?
+- periodStart, periodEnd, generationDurationMs
organizations/{orgId}/scheduledTaskRuns/{runId}
|- taskId, taskType, status, summary, details
+- startedAt, completedAtAgent Evolution Dashboard-only
Closed-loop system that learns from real traffic and safely rewrites the agent
Video coming soon
Click to preview (placeholder)
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
- Observe - sample recent traces for the bot
- Detect - cluster failures & near-misses
- Infer - reviewer LLM names the pattern + root cause
- Generate - draft a rule, prompt edit, or guardrail
- Inject - attach to the bot behind an experiment flag
- 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, and notifications
Run logs
History of every evolution run with status, cost, and artifacts
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
Video coming soon
Click to preview (placeholder)
Agent Skills are modular, reusable, versioned, and measurable capability packages that can be attached to any bot in your organization. When a skill is active, its system instructions are conditionally injected into the bot's system prompt based on trigger conditions — natural-language descriptions matched against the conversation. Skills can be created manually, auto-promoted from evolution rules, imported, or suggested by the reviewer engine. Manage from Dashboard → Agent Skills.
4 skill types (1 runtime-supported)
- prompt_template — reusable instruction pattern injected when triggered ✓ runtime
- tool_chain — ordered sequence of tool calls with parameter mapping future
- code_snippet — programmatic capability executed in one turn future
- workflow — multi-step procedure with conditional branches future
Trigger matching (deterministic, no LLM)
score = 0.4 × (matched_keywords / total) + 0.6 × (trigger_hits / total)
- Activate when score ≥ 0.25 (default), cap at 8 skills/turn
- Keywords extracted from trigger conditions (max 30)
- Optional semantic matching: embedding cosine similarity at weight 0.3
- Always-on skills (no triggers) activate at score 1.0, sort 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"
- Safety scan for prompt injection patterns before rendering (warns, never blocks)
- 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
- Org-level — botId = null, every bot loads it
- Runtime loads bot-scoped first (priority), then org-level
- Status: draft → active → disabled → archived
- Any status can be rolled back to a previous version
Versioning, rollback & auto-promotion
- Every change creates a SkillVersionDoc (7 change types)
- Rollback restores instructions + status + triggers, resets counters
- Auto-promotion: opt-in per-bot, cluster size ≥ 3 (configurable 2–10)
- Promoted skills start as draft (requires operator review)
- Effectiveness: usageCount, successCount, avgConfidence, lastUsedAt
RBAC & plan limits
evolution.edit_config— admin+ (manage skills)- Viewers can see but not create/edit/delete/status/rollback
Free: 3 / Lite: 5 / Pro: 25 / Team: 25 / Enterprise: unlimited
Dashboard tabs
- Skills — list with search, status filter, bot selector, expandable cards with metrics
- Library — browse 50 prebuilt skills across 15 categories, search/filter, one-click install
- Settings — enable Skills Layer, auto-promote toggle, cluster size slider
Enable the Skills Layer toggle in Skills → Settings to allow auto-promotion.
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 tab, click "Add" to install a fork into your org.
Fork model
- Installing creates a copy you own (
source: "prebuilt") - No auto-updates — edit freely after install
- Starts as
draft— review before activating - Org-level by default (botId = null, all bots)
Guardrails
- Duplicate prevention — can't install the same catalog skill twice
- Quota — installs count against the org's max_skills plan limit
- Batch install — "Install All" button respects quota
- Installed skills are normal skills — edit, clone, delete, rollback
Firestore shape
organizations/{orgId}/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
organizations/{orgId}/agentSkills/{skillId}/versions/{versionId}
|- version, changeType, previousInstructions, newInstructions
|- previousStatus, newStatus, changeReason
+- usageCountAtChange, changedBy, createdAt
organizations/{orgId}/agentSkills/{skillId}/usageEvents/{eventId}
|- eventId (turnId:skillId), turnId, outcome, confidence, score
+- createdAt, aggregatedMission Control Dashboard-only
Live human oversight for every production conversation
Video coming soon
Click to preview (placeholder)
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
- 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
organizations/{orgId}/liveConversations/{conversationId}
|- botId, conversationId, status, riskScore, riskLevel
|- riskSignals[], messageCount, durationMs, lastMessagePreview
|- guardrailTriggeredCount, assignedOperatorId, monitoringOperatorIds[]
|- channel, isProduction, tags, flaggedReason
+- startedAt, lastMessageAt, endedAt
organizations/{orgId}/operators/{uid}
|- status: online/away/busy/offline, maxConcurrentMonitors
|- totalInterventions, totalApprovals, avgResponseTimeMs
+- shiftStart, shiftEnd, lastActiveAt
organizations/{orgId}/interventions/{interventionId}
|- type, operatorId, rationale, payload, outcome
|- riskScoreAtTime, messageCountAtTime, idempotencyKey
+- createdAt, resolvedAt
organizations/{orgId}/approvalRequests/{requestId}
|- actionType, urgency, timeoutSeconds, status
|- riskScoreAtTime, riskSignals[], conversationContext
+- requestedAt, expiresAt, resolvedBy, resolvedAt
organizations/{orgId}/alertRules/{ruleId}
|- trigger, severity, config, botId, notificationChannelIds[]
|- cooldownSeconds, snoozedUntil, quietHours
+- lastTriggeredAt, triggerCount
organizations/{orgId}/alertInstances/{alertId}
|- ruleId, status, groupKey, fireCount, evidence[]
+- firstFiredAt, lastFiredAt, acknowledgedAt, resolvedAt
organizations/{orgId}/oversightAudit/{entryId}
|- eventType, operatorId, conversationId, botId
+- details, riskScoreAtTime, timestamp
organizations/{orgId}/missionControl/config
|- enabled, defaultApprovalUrgency, autoMonitorHighRisk
|- complianceMode, auditRetentionDays, maxOperatorsPerConversation
+- pauseMessage, takeoverMessageFlows
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. Gated by the flows plan feature (Pro+).
Flow document (FlowDoc)
name,descriptionmode: guided | strictstart_step_id: entry step (must be defined)steps: FlowStep[]enabled: boolean
Step structure (FlowStep)
id(unique),namegoal: injected into the system prompt for this stepcollect: data fields to slot-fill before advancingallowed_tools: string[] | "all" | "none"allowed_agent_id: sub-agent for this step (groups)terminal: ends the flow
Transition
to: target step idkind: always | data_complete | intent | condition | tool_resultintent(kind=intent),expr(kind=condition)tool(kind=tool_result): fires when it ran successfully this turn
Runtime state (FlowState)
flow_id,current_step_idvisited: step ids seen this sessiondone: flow reached a terminal step- One FlowState per session, advanced each turn
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
/attachso 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; scope allowed_tools; list collect
- 4. advance() evaluates transitions and moves to the next step
- 5. A terminal step marks the flow done
Versioning
- Each update snapshots the prior revision
GET .../versionslists prior revisions- Restore = load a revision into the editor and Save
- Best-effort — a snapshot failure never blocks the save
Plan gating (flows feature)
flowsis a Pro+ feature gate (require_feature)- Not a per-count quota — no max_flows limit
- Free / Lite: not available
- Pro / Team / Enterprise: available
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 the flows feature (Pro+). Internal (dashboard-managed), not public REST.
Firestore shape
organizations/{orgId}/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
organizations/{orgId}/bots/{botId}
+- flowId: string | null
organizations/{orgId}/agentGroups/{groupId}
+- flowId: string | nullDatasetsDashboard-only
Video coming soon
Click to preview (placeholder)
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, ordashboard - 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
| Plan | Max datasets | Max preserved traces |
|---|---|---|
| Free | 1 | 50 |
| Lite | 10 | 5,000 |
| Pro | 50 | 25,000 |
| Team | 50 | 25,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
Video coming soon
Click to preview (placeholder)
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 charsclientName(1-200),redirectUris[]grantTypes: authorization_code, refresh_tokentokenEndpointAuthMethod: none | client_secret_postclientSecretHash: 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, KBrylvo:write— create/update bots, prompts, knowledge, settingsrylvo: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
- CSP: nonce-locked, HSTS, X-Frame-Options DENY
- 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)
- Composite index required on
oauthRefreshTokens.clientId
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
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
organizations/{orgId}/oauthRefreshTokens/{tokenHash}
|- userId, clientId, scope
|- expiresAt (30d sliding), familyMaxExpiresAt (365d ceiling)
|- revoked, revokedAt?
+- createdAt
Token format: rfr_{base64url(orgId)}~{random}
organizations/{orgId}/oauthRevokedJtis/{jti}
|- revokedAt
+- expiresAt (24h, access token TTL)
TTL policies REQUIRED: authCodes (10m), refreshTokens (30d), revokedJtis (24h)Webhook Connectors
Video coming soon
Click to preview (placeholder)
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
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
| Method | Path | Description |
|---|---|---|
| POST | /v1/connectors | Create 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}/activate | Activate |
| POST | .../{id}/deactivate | Deactivate |
| POST | .../{id}/test | Test endpoint |
| POST | /v1/connectors/generate-secret | Generate HMAC secret |
| GET | /v1/connectors/{company_id}/tools/available | List webhook tools |
7 authentication methods
10 subscribable event types
Connector Python SDK
Video coming soon
Click to preview (placeholder)
Typed convenience methods for all connector operations. Sync and async variants included.
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
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 existConnectorAuthError(401/403)Invalid or missing API keyConnectorValidationError(422)Invalid connector configurationConnectorSDKError(5xx)Base class for all SDK errorsChannelsDashboard-only
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 → Channels.
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)
enabledonly 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
botIdandchannelType
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
organizations/{orgId}/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
organizations/{orgId}/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?
organizations/{orgId}/botChannelConfigs/{configId}/dedup/{fingerprint}
|- createdAt, expireAt (24h TTL)
organizations/{orgId}/botChannelConfigs/{configId}/health/events/log/{docId}
|- (append-only event log, eliminates RMW hotspot)
organizations/{orgId}/channelMediaAssets/{assetId}
|- expiresAt (max 30 days), GCS blob referenceMCP HubDashboard-only
Video coming soon
Click to preview (placeholder)
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
Video coming soon
Click to preview (placeholder)
Build Intelligence lets AI editors and agents control Rylvo directly. Connect Claude Code, Cursor, Windsurf, Zed, or Codex CLI via MCP and give them 120+ 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
# Claude Code
claude mcp add --transport http rylvo https://rylvo.com/mcp
# Then run /mcp inside Claude Code and authenticate// Cursor — .cursor/mcp.json or ~/.cursor/mcp.json
{
"mcpServers": {
"rylvo": { "url": "https://rylvo.com/mcp" }
}
}// Windsurf — ~/.codeium/windsurf/mcp_config.json
{
"mcpServers": {
"rylvo": { "serverUrl": "https://rylvo.com/mcp" }
}
}// Zed — settings.json
{
"context_servers": {
"rylvo": { "url": "https://rylvo.com/mcp" }
}
}# Codex CLI
codex mcp add rylvo --url https://rylvo.com/mcp
codex mcp login rylvo --scopes rylvo:read,rylvo:writeTool surface
| Resource | Read | Write | Delete |
|---|---|---|---|
| Bots | bots.list, bots.get | bots.create, bots.update | bots.delete |
| Prompts | prompts.list, prompts.get | prompts.create, prompts.update, prompts.set_optimization | prompts.delete |
| Guardrails | guardrails.list, guardrails.get | guardrails.create, guardrails.update | guardrails.delete |
| Tests | tests.list_cases, tests.list_runs, tests.get_run_results | tests.create_case, tests.run_suite | tests.delete_case |
| Connectors | connectors.list, connectors.get | connectors.create, connectors.update, connectors.test | connectors.delete |
| Channels | channels.list, channels.get | channels.create, channels.update, channels.register_webhook | — |
| MCP Servers | mcp_servers.list, mcp_servers.get | mcp_servers.register, mcp_servers.update | mcp_servers.delete |
| Skills | skills.list, skills.get | skills.create, skills.update | skills.delete |
| Knowledge Base | kb.list_connections, kb.get_connection | kb.create_connection, kb.update_connection, kb.add_source | kb.delete_connection |
| Agent Groups | agent_groups.list | agent_groups.create, agent_groups.add_bot | agent_groups.delete |
| Automations | automations.list_tasks, broadcasts.list | automations.create_task, broadcasts.create | automations.delete_task, broadcasts.delete |
Macro tools
scaffold_bot_from_description— create a bot + system prompt + starter guardrail from a briefclone_bot— duplicate a bot and optionally copy linked resourcesattach_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
Video coming soon
Click to preview (placeholder)
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 - 20+ starting points (SaaS support, fintech KYC, healthcare intake, etc.)
- Org profile prefill - reuses defaults you set once in your organization profile
Knowledge BaseDashboard-only
Video coming soon
Click to preview (placeholder)
Connect your data sources to Rylvo for grounded, evidence-backed decisions. The KB system provides hybrid retrieval with 12 pre-built blueprints covering common enterprise patterns.
How it works
Add a source
Upload documents, connect APIs, or point to URLs. 16 source types supported.
Pick a blueprint
Choose from 12 retrieval blueprints (FAQ, Policy Lookup, Troubleshooting, etc.) or create custom.
Create a connection
A connection links a source to a blueprint with specific pipeline settings.
Query in playground
Test retrieval quality before going live. Tune chunking, reranking, and thresholds.
Built-in blueprints
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
Vector store
Qdrant stores chunked embeddings. Keyword + semantic search are combined, then reranked for relevance.
BYOK decryption
If a source or org uses a BYOK provider key, it is decrypted at retrieval time before calling the embedding model.
Retrieval trace
Every KB call is recorded in the canonical observability store as one clean retriever observation per connection, with latency, score, and strategy.
Citations
Grounded answers include source references with chunk IDs so you can audit what the bot actually saw.
Conversation StorageDashboard-only
Video coming soon
Click to preview (placeholder)
Bring Your Own Database (BYODB). Every conversation event Rylvo processes can be pushed to a database or webhook endpoint you own — so your conversations live permanently in infrastructure you control. Rylvo also maintains a time-bounded operational copy for Mission Control, traces, agent evolution, and edge-case detection. Manage from Dashboard → Conv. Storage.
Connections
Add one or more storage targets. Each target receives a copy of every conversation event.
- PostgreSQL, Supabase, Neon, PlanetScale
- MySQL, Turso, MongoDB, Redis
- Webhook (none / API key / Bearer / HMAC auth)
- Credentials encrypted at rest
Schema Designer
Map Rylvo's conversation fields to your own table columns or JSON keys.
- Custom field mapping per target
- Preview the output schema and test DDL before going live
- Drivers only INSERT — you run the DDL on your side
Sync Logs + Dead Letter
Per-event push history with status, latency, and error details.
- Success / failure / retry status per target
- Webhook retry with exponential backoff (0.5 s → 1 s → 2 s)
- Inspect the exact payload that was sent
- Dead Letter Queue for terminal failures
- Live / Test toggle isolates playground traffic
Retention
How long Rylvo keeps its own operational copy of your conversations.
Production / Test isolation
Every storage event carries a source discriminator:channel (live), playground (dashboard chat), or test_suite (automated eval). Live Saves, Sync Logs, and Dead Letter all have a Production / Test toggle so you can inspect playground traffic separately.
Live (channel)
- Deployed-channel traffic: Telegram, WhatsApp, widget, API, etc.
- Writes to base tables (e.g.
rylvo_conversations) - Uses the production circuit-breaker key
- Billed normally
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
sourceandisTest - Billed exactly like production events
Run the test DDL first. Drivers only INSERT; they never auto-create tables. The Schema Designer emits the production DDL and a matching *_test DDL. Create the test tables before flipping playground traffic to a target.
Your database is the source of truth. Rylvo's copy is an operational cache — not a backup. Use the Connections tab to wire up your own database for permanent storage. Extending Rylvo-side retention increases storage costs but does not replace your own database.
Audience HubDashboard-only
Video coming soon
Click to preview (placeholder)
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/knavigate,Enterinspect
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 countretentionPolicy: { 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,addedAtsnapshot: 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:
PlanAccessErrorcode="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
organizations/{orgId}/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
organizations/{orgId}/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' countedBroadcastsDashboard-only
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
organizations/{orgId}/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
organizations/{orgId}/botBroadcastRuns/{runId}
|- status: "success" | "partial" | "failed"
|- perChannelStats: { [channel]: { sent, failed, skipped } }
|- targetCount, sentCount, failedCount, skippedCount
+- generatedContent, errors[]Team & RBACDashboard-only
Role-based access control for every dashboard surface. Four ranked roles, email invitations, join requests, granular page-access grants, ownership transfer, and SSO (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, manage SSO
- 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— owneraudit.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. 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: Enterprise plan only, owner-only capability
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
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?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
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 labelkeyPrefix— first 12 chars for displaykeyHash— SHA-256 of raw key (never store raw)environment— production | staging | developmentpermissions— API scopes arrayrateLimitOverride— 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 (4 + 2 wildcards)
respond— POST /api/v1/bot-run + widget-pollkb:read— POST /api/v1/kb/querykb:write— POST /api/v1/kb/documentstrace.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
rateLimitOverridefield - Token bucket via
consumeTokenUnified() - 429 response with
Retry-Afterheader - Per-route buckets (bot-run, kb/query, etc.)
RBAC & plan limits
apiKeys.view— admin+apiKeys.create— admin+apiKeys.revoke— admin+- Free: 1 | Lite: 10 | Pro: 25 | Team: 25 | Enterprise: unlimited
- 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)
Auth: API key via Bearer / X-API-Key / X-Rylvo-Key header. Rate limited per key.
Firestore shape
organizations/{orgId}/apiKeys/{keyId}
|- name, keyPrefix (12 chars), keyHash (SHA-256)
|- environment: production | staging | development
|- 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_approvalpermission). 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_idformat: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_deniedstatus
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 eventapprove()— verify pending, update, log, idempotentdeny()— verify pending, update + reason, log, idempotentlist_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_approvalcapability: 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
organizations/{orgId}/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
organizations/{orgId}/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: TimestampNotificationsDashboard-only
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,typetitle,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
60+ types across 8 categories
- team (8): join_request, member_joined, page_access_*
- mission_control (7): alert_rule, guardrail_escalation, channel_error, byok_unhealthy, wallet_low
- retention (2): retention_warning, retention_deleted
- edge_cases (2): edge_case_alert, guardrail_triggered
- billing (10+): low_balance, credits_low, quota_exceeded, subscription_*
- broadcasts (1), system (2): broadcast, welcome
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 channelbotIds?: 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
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? }
organizations/{orgId}/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.
