# epilot dev center - Complete Documentation epilot is a cloud-native SaaS platform (Energy XRM) designed for energy suppliers and service providers in the German energy market. The platform provides: - **Entity Management**: Flexible data modeling with schemas for contacts, products, orders, opportunities, and custom entities - **Journey Builder**: Visual designer for customer onboarding journeys, self-service portals, and multi-step forms - **Workflow Engine**: Configurable workflows for business process automation with task management and SLA tracking - **Automation Engine**: Rule-based automation for entity mapping, document generation, and integrations - **Portal Framework**: Customer (ECP) and installer portals with granular permissions and self-service capabilities - **Messaging Hub**: Centralized email and messaging with templates and shared inboxes - **Integration Toolkit**: Pre-built connectors for SAP, Wilken, and other ERP/billing systems - **REST APIs & SDK**: Comprehensive APIs for all platform capabilities with TypeScript SDK - **Document Generation**: Template-based document creation with variable support - **Pricing & Product Catalog**: Flexible product management with tiered pricing and availability rules - **AI Features**: AI-powered copilot for entity management, messaging, and workflow automation - **Apps & Marketplace**: Extensibility platform for third-party and custom applications This documentation covers all aspects of the epilot platform for developers, administrators, and integration partners. --- Every tool exposed by the epilot MCP server at mcp.epilot.io, grouped by purpose, with access requirements. # epilot MCP server `https://mcp.epilot.io/mcp` is the official, hosted MCP server for the epilot platform. It is built for the person **configuring** epilot: solution engineers, administrators, partners taking over an organization. It answers three questions well: 1. **How is this organization set up?** 2. **What is connected to what?** 3. **What breaks if I change X?** For everything else it exposes the full published OpenAPI catalog, so any epilot API operation can be discovered, described, and executed through one generic route. ## Design principles - **Curated only where it earns its slot.** A dedicated tool exists when a workflow spans several APIs (journey plus design plus mapping plus automation), when validation should happen before a write, or when a raw payload would not fit a model context (tenant schemas are hundreds of kilobytes). Plain wrappers over single API operations were removed on 9 September 2026; those reads go through `search_configuration` and `call_api_operation`. - **Read by default.** The OAuth scopes are `mcp:read` and `mcp:write`. Read-only is preselected on the approval screen. A connection over `/mcp?access=read` is enforced read-only. - **Credentials never leave the server.** Responses from webhook, journey, and portal configuration endpoints have `auth` blocks, signed journey tokens, and Cognito wiring stripped. The removed paths are listed in `redacted_fields`, so a missing value is reported as redacted, not unset. - **Your permissions, always.** Tools run as the signed-in user. The server never trusts a caller-supplied organization, and every upstream API applies its own permission checks. - **PII masked.** Entity data on OAuth connections is anonymized server-side. `whoami` reports this as `entity_pii`. ## Tools Access column: **Read** works with `mcp:read`. **Write** requires the `mcp:write` scope. ### Connection | Tool | Purpose | Access | | -------- | ------------------------------------------------------------------------------------------------------- | ------ | | `whoami` | Organization, user, auth mode, granted scopes, and PII anonymization state. Call it first in a session. | Read | ### Configuration graph Powered by the Configuration Hub, which indexes 33 resource types. This is also how you list things: journeys are `type: journey`, webhooks `webhook`, portals `portal_config`, designs `designbuilder`, entity schemas `schema`, automations `automation_flow`. | Tool | Purpose | Access | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | `search_configuration` | Without arguments: an org-wide inventory with counts per type. With a query or a type: find resources by name or alias, or list one resource type. | Read | | `get_config_dependencies` | Forward edges: what a resource references, for example the automations, products, and email templates a journey uses. | Read | | `get_config_impact` | Reverse edges: what references a resource. Check before changing or deleting anything. An empty result with `index_status` not `ready` means unknown, not safe. | Read | ### Entity model | Tool | Purpose | Access | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | `get_entity_schema` | Compact tenant schema: attribute names, types, required flags. Verify attributes here before touching journeys, automations, or mappings. Slugs come from `search_configuration` with type `schema`. | Read | Entity records are searched on the generic route: `call_api_operation` with `searchEntities` and a Lucene query such as `_schema:contact AND first_name:Erika`. It is a read-equivalent POST and runs with `mcp:read`. ### Journeys Journey writes on the generic route are redirected to these tools because they would bypass validation and mapping sync. The signed journey token is never returned. | Tool | Purpose | Access | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | `get_journey` | One journey. `view=full` is the editable definition as accepted by `update_journey`. `view=summary` reduces steps to block names and shows logic wiring, injection rules, context parameters, and state flags. | Read | | `validate_journey_definition` | Structural checks on journey steps with no API call: unique step IDs, schema keys matching every uischema scope, button targets pointing at existing steps. Returns the same problems the write tools would reject with, so a definition can be iterated on a read-only connection. | Read | | `create_journey` | One call creates a working journey end to end: optionally a new design, the journey, the automation that maps submissions into entities, and optionally its mapping targets. Each part is reported as created or skipped with the reason. New journeys start inactive. | Write | | `update_journey` | Replace whole sections (name, steps, logics, rules, contextSchema, settings). Omitted sections stay unchanged. Step wiring is validated first. | Write | | `get_journey_mapping` | How submissions map into entities: versioned mapping targets and the executing automation, including `safeModeAutomation`. | Read | | `update_journey_mapping` | Store mapping targets as a new version and keep the automation in sync. Skipped when `safeModeAutomation` is on. Conflict detection on version. | Write | ### Designs Designs hold journey and portal branding. Find them with `search_configuration` type `designbuilder`; read one with `call_api_operation` and `getDesign`. | Tool | Purpose | Access | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | `create_design` | Create a design. A style name and palette are enough; typography is inherited from the default design. Can also be passed inline to `create_journey`. | Write | | `update_design` | Replace provided sections of a design; design tokens merge field by field. Affects every journey and portal using it, so check `get_config_impact` first. | Write | ### API discovery and execution The generic route for every published epilot API, including webhooks, portals, automations, workflows, products, and entity records. | Tool | Purpose | Access | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | | `search_api_operations` | Search operationIds, summaries, tags, and paths across all published OpenAPI specs. Omit the query to list every operation, optionally scoped to one service. | Read | | `describe_api_operation` | Method, path, parameters, request body, responses, security, and referenced schemas for one operationId. | Read | | `call_api_operation` | Execute an operation by operationId. The URL always comes from the trusted catalog, never from the caller. `GET` and the read-equivalent POSTs `searchEntities`, `simulateMapping`, and `simulateMappingV2` run with `mcp:read`; every other `POST`, `PUT`, `PATCH`, `DELETE` requires `mcp:write`. Large responses can be projected with the `response_filter` JSONata parameter. | Read / Write | ### Documentation | Tool | Purpose | Access | | ------------- | ----------------------------------------------------------------------- | ------ | | `search_docs` | Search the published epilot documentation for how-to and concept pages. | Read | | `fetch_doc` | Read one documentation page as Markdown, by URL from `search_docs`. | Read | Both tools are limited to `docs.epilot.io`. ## Typical flows **Understand an organization** 1. `search_configuration` with no arguments for the inventory. 2. `search_configuration` with a query or type to find the resource you care about. 3. `get_config_dependencies` and `get_config_impact` to walk the graph in both directions. **Create a journey** 1. `get_journey` with `view=full` on a proven journey to copy block shapes instead of inventing them. 2. `validate_journey_definition` until the wiring is clean. This works on a read-only connection. 3. `create_journey` with the definition, an optional inline design, and mapping targets. Read the per-part report. 4. `get_journey_mapping` and `get_entity_schema` to confirm the mapping targets exist. **Change a journey safely** 1. `get_config_impact` on the journey to see what depends on it. 2. `get_journey`, edit, then `update_journey` with only the changed sections. 3. `get_journey_mapping` to check whether new blocks need mapping. **Call any API** 1. `search_api_operations` with a keyword, or scoped to a service. 2. `describe_api_operation` to see the request shape. 3. `call_api_operation` with the operationId and parameters. Use `response_filter` to keep large responses small. **Iterate on an inbound mapping** `simulateMappingV2` from the Integration Toolkit computes the resulting entity updates from a mapping and a sample payload without persisting anything, and runs with `mcp:read`. Simulate until the output is right, then store the configuration. ## Authentication | Mode | Best for | How | | ---------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | OAuth 2.1 | Claude, ChatGPT, Cursor, and other interactive clients | The client discovers the authorization server, registers dynamically, and opens the epilot 360 login. Public clients with PKCE only. | | epilot API token | Claude Code with `.mcp.json`, CI, service accounts | Send the token as the Bearer header. The organization and roles are resolved from the token. | OAuth connections create a dedicated integration token named after the client and user, for example `MCP: Claude (Erika)`. It is visible and revocable in epilot 360 token settings, and revoking the MCP connection deletes it. ## Protocol The server speaks the current MCP specification over stateless streamable HTTP and remains compatible with 2025-era streamable HTTP clients. Every request creates a fresh server; no session IDs are issued. Only tools are exposed. There are no resources or prompts. ## Limits - Automation executions can only be queried per entity upstream, so org-wide failure triage is not available yet. - Organizations can hold thousands of journeys. Narrow `search_configuration` with a query rather than listing the whole type. - Large API responses are truncated with a note. Use `response_filter`, narrower queries, or pagination parameters. ## Changelog - **2026-09-09**: Removed plain wrappers `list_webhooks`, `list_portals`, `describe_portal`, `list_designs`, `get_design`, `list_journeys`, `describe_journey`, `list_entity_schemas`, `search_entities`. Added `get_journey` with `view` and `validate_journey_definition`. `create_journey` now creates design, journey, mapping automation, and mapping targets in one call. Credential redaction moved into the generic route. --- The Agent Toolkit for epilot: the epilot plugin (skills) and the epilot MCP server, and when to use which. # Agent Toolkit for epilot epilot offers the **[Agent Toolkit](https://github.com/epilot-dev/agent-toolkit-for-epilot)** so that third-party AI assistants — Claude, Codex, ChatGPT, and others — can work with epilot for you. Connect it once, and your AI assistant can answer questions about how your organization is set up ("Which journeys feed into this workflow?") and build or change configuration on your behalf ("Create a workflow for new solar orders") — in plain language, no API knowledge required. The toolkit is packaged as a **plugin** following the open [Agent Plugins specification](https://agent-plugins.org/), a vendor-neutral standard for extending AI assistants. A plugin bundles two kinds of things: - **Skills** — written know-how the assistant reads before it acts: epilot's concepts, decision rules, and proven step-by-step workflows. Skills make the assistant work *the epilot way* instead of guessing. - **MCP servers** — live tools (via the [Model Context Protocol](https://modelcontextprotocol.io/)) the assistant calls to look up your organization's real configuration and to create or update things — always with your explicit approval, and read-only unless you opt into write access. ![The epilot plugin page in the marketplace, with starter prompts and its two MCP servers](/img/agent-toolkit/plugin-marketplace.png) The first time you install the plugin — or call an epilot MCP tool — you are redirected to the epilot login: sign in, pick the organization the assistant should work with, and choose the access level. **Read-only is preselected**; read & write is an explicit choice. ## Where can you use it? The full plugin — skills included — works in AI clients that support Agent Plugins, such as **Claude Code**, **Codex**, and the **ChatGPT desktop app** (imported by a workspace admin). Clients that don't support plugins yet, like **Claude Cowork** or the regular ChatGPT web app, can't run the skills for now — that may change as those products add plugin support. They can still connect the **epilot MCP server** directly as a connector, which gives the assistant the live tools without the packaged know-how. > **Info: Plugins and connectors may be disabled in your company** Many companies block MCP connectors and plugins by default for security reasons. If you don't see a way to add the epilot plugin or connector in your AI client, ask your workspace or IT administrator to enable it — admins can typically allow a specific connector (like `https://mcp.epilot.io/mcp`) for all users, or grant it to selected roles. The [setup guide](/docs/agent-toolkit/setup) has the admin steps per client. ## What do you do with it? Nothing about how you chat changes. You describe the outcome you want in everyday language, and the assistant uses the toolkit behind the scenes. In some clients you address it explicitly — in ChatGPT, for example, you type `@epilot` followed by your request. In others, like Claude, the assistant automatically reads the plugin's skills and calls the MCP server when it needs live information. Typical things to ask: - *"How is my organization set up? What depends on this journey?"* — the assistant reads your live configuration and explains it. - *"Create a workflow for new orders with a review step."* — the assistant drafts it, shows you the result, and only writes after you approve. - *"What would break if I renamed this attribute?"* — the assistant walks the dependency graph before you touch anything. Under the hood, this is the toolkit's biggest convenience: instead of you (or the AI) stitching together many raw API requests, the assistant makes **one tool call** — such as `create_workflow` or `create_journey` — and the epilot MCP server acts as a **facade** that validates the input and performs the underlying API calls in the right order: ## The typical workflow: sandbox first Letting an AI assistant change a live organization is powerful — so don't point it at production. The workflow we recommend: 1. **Connect the plugin or MCP server to a [sandbox organization](/docs/blueprints/sandboxes)** and grant read-and-write access there. A sandbox is a full epilot organization with isolated test data, linked to your production organization. 2. **Let the assistant build and change configuration in the sandbox** — journeys, workflows, schemas, automations — and review the result in the epilot UI, at no risk to live customers. 3. **Synchronize to production through the Configuration Hub**: package the changes as a Blueprint and [synchronize the Blueprint](/docs/blueprints/editing-and-synchronizing#synchronizing-your-blueprint-with-another-org) to your production organization once you are happy with the setup. **Switching between organizations** (for example from the sandbox to production, or between two sandboxes) is easiest by simply reinstalling the plugin — the fastest way to trigger the login again and pick a different organization: > **Caution: Keep production read-only** Granting the assistant write access to your production organization is possible, but not recommended — you do so at your own risk. Connect production with the read-only URL (`https://mcp.epilot.io/mcp?access=read`) so the assistant can answer questions about your live setup but can never change it, and keep write access confined to the sandbox. To take the choice away entirely: in enterprise editions of ChatGPT and other major AI providers, administrators can allow only `https://mcp.epilot.io/mcp?access=read` in the MCP configuration for the production organization — the read/write selection then never appears on the login screen. ## The two parts of the toolkit You can use the two parts together or separately: | | **epilot plugin** | **epilot MCP server** | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | What it is | A package of skills (guidance, decision rules, workflows) plus MCP configuration | A hosted server at `https://mcp.epilot.io/mcp` that exposes tools | | What it gives the agent | _How_ to work with epilot: architecture choices, App and integration patterns, configuration workflows, native UI design | _What is true right now_: your organization's configuration, entity schemas, published APIs, and documentation | | Runs where | Inside the agent client (Claude Code, Codex, the ChatGPT desktop app, and other Agent Plugins clients) | On epilot infrastructure; any MCP client can connect | | Needs | A client that supports Agent Plugins | A client that supports remote MCP over HTTP with OAuth | | Identifier | `epilot-core` from the `agent-toolkit-for-epilot` marketplace | `https://mcp.epilot.io/mcp` | The plugin **includes** the MCP server configuration. Installing the plugin connects the MCP server for you. Connecting the MCP server alone does not install the skills. ## When to use what **Use the plugin** when the agent will build or configure something and you want it to follow epilot's proven patterns: - Building an App with the CLI, manifest, and App Bridge. - Deciding between native configuration, an App, or an external integration. - Configuring schemas, journeys, products, workflows, automations, or portals end to end. - Designing App surfaces that look native with Volt UI components and tokens. The plugin's skills route the task to the right workflow and load only the relevant guidance. They rely on the MCP server for live facts, so the two work best together. **Use the MCP server alone** when you only need live access to an organization or the platform contracts: - Your client does not support Agent Plugins but does support MCP (Claude.ai, Claude Cowork, Cursor, VS Code, custom agents). - You are asking questions about an organization: how it is set up, what depends on what, what would break if something changed. - You are building your own agent and want tools rather than prompts. - Compliance requires a read-only connection. Connect `https://mcp.epilot.io/mcp?access=read` and writes are impossible regardless of consent. **Use neither** when the task is plain code against the public APIs. The [SDK](/docs/sdk/overview) and [CLI](/docs/cli/overview) are lighter, and `llms.txt` at `https://docs.epilot.io/llms.txt` gives any model the documentation index. ## MCP server or CLI? There is a third way for agents to reach epilot: the [epilot CLI](/docs/cli/overview) (`npx epilot`). Many platforms pair their agent tooling with a CLI — Datadog's `pup` CLI is a well-known example — because agents that already live in a terminal, like Claude Code and Codex, can drive a CLI without any connector setup: they discover operations through `--help`, call any API operation directly, and get `--json` output that is easy to parse. The rule of thumb: - **Use the MCP server** when the agent runs in a chat client without a terminal (Claude.ai, Claude Cowork, ChatGPT), when you want the OAuth consent flow with enforceable read-only access, or when you want the curated facade tools (`create_workflow`, `create_journey`, configuration graph, journey validation) instead of raw endpoints. - **Use the CLI** when the agent has shell access and the task maps to plain API operations — quick lookups, scripting, CI pipelines, or piping results through `jq`. It is the leanest option: one command per API call, no server in between. They complement each other rather than compete: the plugin's skills reach for the MCP server's tools for live configuration work, and a terminal agent can mix in CLI calls whenever a raw operation is all that is needed. ## What is inside the plugin The `epilot-core` plugin ships six skills and two MCP servers. | Skill | Use it for | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | epilot platform guide | Explains entities, relations, journeys, workflows, Apps, and routes a requirement to configuration, App, or integration | | Build an epilot App | Scaffolds, extends, validates, and troubleshoots Apps with the current CLI, manifest schema, and App Bridge | | Integrate with epilot | Designs inbound, outbound, batch, webhook, and bidirectional connections without an App | | Configure epilot | Sets up schemas, journeys, products, pricing, workflows, automations, portals, and permissions | | Build an epilot journey | Creates and updates customer-facing forms and funnels, including logic, copy, and design | | epilot interface designer | Uses live Volt UI component and token guidance to make custom surfaces feel native | | MCP server | Transport | Purpose | | ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `epilot` | Remote HTTP, `https://mcp.epilot.io/mcp` | Configuration graph, schemas, journeys, portals, API discovery and execution, documentation. See the [MCP server reference](/docs/agent-toolkit/mcp-server). | | `volt-ui` | Local, `npx -y @epilot/volt-ui-mcp` | Volt UI components, props, and design tokens for App interfaces. Needs Node.js 22+. | ## Next steps - [Set up the toolkit](/docs/agent-toolkit/setup) in Claude, ChatGPT, Codex, or another MCP client. - [Browse the MCP server capabilities](/docs/agent-toolkit/mcp-server). - Source: [agent-toolkit-for-epilot on GitHub](https://github.com/epilot-dev/agent-toolkit-for-epilot). --- Install the epilot plugin or connect the epilot MCP server in Claude, ChatGPT, Codex, and other MCP clients. # Set up the Agent Toolkit Pick your client. Every route ends with the same step: the client opens the epilot login, you choose an organization, and you choose the access level. **Read-only is preselected**; read-and-write is an explicit choice. To change it later, disconnect and connect again. ## Verify the connection Ask the agent to run `whoami`. The answer names the organization, the user, the authentication mode, the granted scopes (`mcp:read` or `mcp:write`), and whether entity data is PII-anonymized. If a task needs a write and the connection is read-only, the server returns a reauthorization challenge; reconnect and approve write access. ## Permissions and data protection - Every tool runs as the signed-in epilot user in the chosen organization. Upstream APIs enforce their normal permissions on every call. - OAuth connections create a dedicated integration token that is visible and revocable under epilot 360 token settings. Revoking the connection deletes it. - Entity data returned to OAuth connections is PII-anonymized server-side by default. The client cannot disable this. - Journey tokens, webhook secrets, and portal auth infrastructure are never returned. The curated tools project safe fields only, and the generic API route blocks the operations that would leak them. --- # AI in epilot epilot embeds AI directly into the workflows energy companies use every day -- messaging, entity management, search, and process automation. Rather than a standalone chatbot, every AI feature operates within existing platform screens, augmenting the work users already do. The system is built on a centralized **GenAI API** backed by AWS Bedrock, a **GenAI Foundation** for LLM orchestration, and a serverless RAG pipeline for contextual generation. All data stays in the EU region with a zero-retention policy on the LLM provider side. ## AI Features ### Email Assistance The messaging hub is epilot's highest-traffic surface. AI augments it in three ways: | Capability | Description | |---|---| | **Thread Summaries** | Bullet-point summary, topics, and suggested next steps generated for every inbound email thread. Reduces comprehension time from 16+ minutes to under 1 minute. | | **Reply Suggestions** | Context-aware draft replies powered by RAG. The system retrieves similar past conversations from a vector store, then generates a reply grounded in prior organizational knowledge. | | **Auto-Labeling** | Inbound emails are classified against a master label list using zero-shot classification. Labels with confidence scores above a per-label threshold are applied automatically; users can accept or reject suggestions. | Thread summaries trigger asynchronously on message creation via EventBridge. Reply suggestions are generated on-demand with streaming support. See [Messaging](/docs/messaging/message-api) for the broader messaging architecture. ### Suggested Actions (AI Agents) When a user views an email, the AI agent reads the message content alongside related entities and proposes concrete actions -- updating a contact's address, changing bank details, creating a meter reading, or registering a new customer. Each suggestion is a tool call with pre-filled arguments that the user reviews and executes with a single click. Under the hood, this uses a **ReAct (Reason and Act) agent** built with LangGraph. The LLM reasons about what needs to happen, selects the right tool (`update_entity`, `create_entity`, `create_meter_reading`), and populates the arguments. Human-in-the-loop approval is enforced -- the agent never writes data without user confirmation. ```mermaid sequenceDiagram participant User participant GenAI API participant LLM Agent participant Entity API User->>GenAI API: View email thread GenAI API->>LLM Agent: Email content + related entities LLM Agent->>LLM Agent: Reason about required actions LLM Agent-->>GenAI API: Proposed tool calls GenAI API-->>User: Display suggested actions User->>GenAI API: Approve action GenAI API->>Entity API: Execute tool call Entity API-->>User: Entity updated ``` ### Entity Summaries AI generates a concise summary for any entity (opportunity, contact, contract, ticket) by synthesizing entity attributes, activity history, related entities, files, and workflow state. Summaries include priority assessment, current status, and recommended next actions. Summaries support multiple variants: - **Short** -- headline-level overview - **Detailed** -- full context with activity timeline - **Action Points** -- prioritized list of next steps Available in both German and English, streamed to the UI in real time. ### Ask Your Entity A conversational interface in the entity side panel. Users ask questions about any entity in natural language: - "What needs to be done next?" - "Did the customer upload the roof picture?" - "When was the bank account last updated?" The LLM answers based on entity payload, related entities, files, emails, notes, activity history, and workflows. This is the foundation for a broader **AI Copilot** that will gain tool access to take actions across the platform. ### AI-Powered Table Filtering Users type natural language queries like "orders with tag solar from last month" into entity tables. The system converts this to structured filter parameters using the LLM, hydrated with the table's full filter schema (including nested relation fields). Results are applied instantly to the table view. ### Blueprint Summaries When exporting [blueprints](/docs/blueprints/intro), AI generates a natural language description of the blueprint contents -- schemas, automations, journeys, and workflows included. This replaces manual description writing by the Customer Success team and accelerates marketplace publishing. ### CSS Assistant for Journeys A conversational AI assistant embedded in the [Journey](/docs/journeys/embedding) Design Builder. Users describe styling changes in plain language ("make all primary buttons use our brand green with rounded corners"), and the assistant generates valid CSS targeting epilot's Concorde design system components. Generated styles are applied live to the journey preview. ### Portal Design Generation AI generates portal theme configurations from a reference URL or natural language description, enabling rapid portal branding without manual CSS work. ### ERP Mapping Generation For [integration](/docs/integrations/overview) scenarios, AI assists in generating and updating ERP-to-epilot field mappings from natural language instructions, reducing the complexity of configuring data synchronization. ## Architecture ### System Overview ```mermaid graph TB subgraph "epilot 360 Frontend" UI[Platform UI] end subgraph "API Layer" GAPI[GenAI API
TypeScript / OpenAPI] CORE[Core APIs
Entity, Message, etc.] end subgraph "AI Processing" SQS[SQS Queue
Rate-limited] GF_SQS[SQS Handler
Async jobs] GF_SYNC[Sync Handler
On-demand] GF_STREAM[Streaming Handler
Real-time] end subgraph "LLM & Retrieval" BEDROCK[AWS Bedrock
Claude Models] WEAVIATE[Weaviate
Vector DB] LC[LangChain / LangGraph] end subgraph "Storage & Observability" DDB[(DynamoDB
Jobs & Feedback)] CH[(ClickHouse
Analytics)] LS[LangSmith
Tracing] end UI --> GAPI UI --> CORE CORE -->|Events| SQS GAPI --> SQS GAPI --> GF_SYNC GAPI --> GF_STREAM SQS --> GF_SQS GF_SQS --> LC GF_SYNC --> LC GF_STREAM --> LC LC --> BEDROCK LC --> WEAVIATE GF_SQS --> DDB GF_SYNC --> DDB GAPI --> DDB DDB --> CH LC --> LS ``` ### GenAI API The **GenAI API** is the centralized gateway for all AI features. Built in TypeScript with the standard epilot OpenAPI stack, it handles: - **Job management** -- every AI operation creates a job tracked in DynamoDB with status (`INITIATED`, `IN_PROGRESS`, `COMPLETED`, `FAILED`), token usage, and model metadata. - **Caching** -- repeated requests for the same input return cached results (e.g., thread summaries keyed by `THREAD_INFO#THREAD_ID#MESSAGE_ID`). - **Rate limiting** -- per-organization limits (RPM/TPM) tied to pricing tiers, using a fixed-window algorithm. - **Feedback collection** -- unified endpoints for user ratings (`LOVE`, `LIKE`, `DISLIKE`) and free-text feedback across all features. The API ships independently from core platform APIs, enabling rapid iteration on AI features without affecting other services. ### GenAI Foundation The **GenAI Foundation** is a Python monorepo that handles all LLM interaction. It runs as Lambda functions behind three entrypoints: | Handler | Use Case | |---|---| | **SQS Handler** | Async jobs (summaries, auto-labeling). Processes messages from the central SQS queue with controlled `batchSize`, `batchWindow`, and `maxConcurrency` to respect Bedrock rate limits. | | **Sync Handler** | On-demand synchronous calls (table filters, suggested actions). Returns LLM output directly to the caller. | | **Streaming Handler** | Real-time streaming responses (reply generation, entity summaries). Uses FastAPI with AWS Lambda Web Adapter. | Both handlers share the same module interface, making it straightforward to expose any feature as either sync or async. ### LLM Provider epilot uses **AWS Bedrock** as the primary LLM provider: - **Models**: Anthropic Claude (Sonnet class) for most features - **EU-only processing**: All inference runs in EU regions - **Zero-retention policy**: Prompts and outputs are not stored by AWS or model providers - **Cross-region inference**: Endpoints span multiple regions to increase available throughput - **Provider flexibility**: Bedrock's multi-model architecture avoids vendor lock-in; migration to alternative providers (e.g., Google Vertex AI) can happen per-feature based on performance data > **Note: All AI processing runs exclusively in EU AWS regions with a zero-retention policy on the LLM provider side, ensuring compliance with European data residency requirements.** ### RAG Pipeline For features that need organizational context (reply suggestions, entity Q&A), epilot runs a Retrieval-Augmented Generation pipeline: 1. **Ingestion** -- Incoming emails are cleaned, PII is redacted using Microsoft Presidio, and hypothetical questions are generated to improve retrieval. Text and questions are embedded and stored in Weaviate. 2. **Retrieval** -- For each user request, the system extracts key questions, runs parallel hybrid searches (keyword + semantic, `alpha=0.90`) across text and question vector fields, and merges unique results above a similarity threshold (0.70). 3. **Augmentation** -- Retrieved documents populate XML-tagged sections in the system prompt alongside entity references, security guidelines, and temporal context. 4. **Generation** -- The augmented prompt is sent to the LLM, which generates a grounded response with inline citations referencing source messages.
Prompt engineering techniques epilot's prompts use several techniques optimized for Claude models: - **XML tags** for structured input sections (``, ``, ``) - **Few-shot examples** showing the expected JSON output format - **Response prefilling** to constrain output format (e.g., prefilling `{` to force JSON) - **Role assignment** in system prompts ("You are an intelligent assistant specialized in assisting customer service agents in the energy industry") - **Structured output** via Pydantic models for validation
### Observability AI features are monitored through: - **LangSmith** -- Native LangChain observability for tracing chains, tracking I/O prompts, comparing model performance, and managing prompt versions - **CloudWatch** -- Invocation counts, error rates, and token usage at the infrastructure level - **ClickHouse** -- Job analytics, feedback aggregation, and per-organization usage reports piped from DynamoDB ### Rate Limiting AI requests are rate-limited per organization to ensure fair usage and consistent performance. Limits scale with your pricing plan. Rate limit headers (`X-RateLimit-Limit-Requests`, `X-RateLimit-Remaining-Requests`, `X-RateLimit-Limit-Tokens`, `X-RateLimit-Remaining-Tokens`, `X-RateLimit-Limit-Reset`) are returned on every GenAI API response. Contact your account manager for details on the limits for your plan. ### Human-in-the-Loop > **Tip: Every AI feature that modifies data enforces human approval. The AI agent never writes data without user confirmation.** The platform collects structured feedback (ratings, rejection reasons, free-text comments) which feeds back into prompt refinement and retrieval quality improvements. Feedback is continuously reviewed to improve AI quality. ## Roadmap The AI roadmap progresses through three phases: 1. **AI-Augmented Workflows** (current) -- Summaries, suggestions, and classifications embedded in existing screens. 2. **AI Copilot** -- A platform-wide conversational interface with tool access to Entity, Messaging, File, and Workflow APIs. Context-aware based on the user's current view. 3. **Autonomous Agents** -- Multi-agent orchestration integrated with Flows (epilot's workflow engine). AI agents can execute workflow steps, draft communications, and escalate to humans when uncertain. A supervisor agent routes tasks to specialized agents (Entity Management, Communication, etc.). ```mermaid graph LR A[Phase 1
AI-Augmented
Workflows] --> B[Phase 2
AI Copilot
Conversational UI] B --> C[Phase 3
Autonomous
Multi-Agent System] ``` --- Interested in building the future of AI for the energy industry? See our [open positions](https://www.epilot.cloud/en/company/careers). --- # The App Manifest `manifest.json` is the **declarative source of truth** for your entire app. It lives in the root of your app repository, is updated by the [CLI](/docs/apps/cli), and every `epilot app deploy` syncs the platform to exactly what it declares — components and functions not in the manifest are removed from the deployed version. ```json title="manifest.json" , "category": "integration", "author": , "documentation_url": "https://docs.acme.example", "support_email": "support@acme.example", "pricing": , "notifications": , "permissions": [ ], "options": [ , ], "assets": , "functions": [ ], "components": [ , "configuration": , "surfaces": }, "assets": } ] } ``` The `$schema` reference gives you validation and autocompletion in editors like VS Code. ## Top-level fields | Field | Required | Description | |---|---|---| | `manifest_version` | ✓ | Always `1` | | `app_id` | — | Set automatically after the first `deploy` — never set it by hand | | `name` | ✓ | App name shown everywhere | | `description` | ✓ | `de` is required; `en` optional | | `category` | — | Marketplace category, e.g. `integration` | | `author` | — | `company` required when set | | `pricing` | — | `FREE`, `SUBSCRIPTION`, `USAGE_BASED`, `ONE_TIME`, `CUSTOM` | | `notifications` | — | Email + events (`app.installed`, `app.uninstalled`) you want to be notified about | | `permissions` | — | The grants your app's server-side code needs — shown to the installing org for consent, see [Permissions](/docs/apps/configure-permissions) | | `options` | — | Settings the installing org fills in — shared by all components and functions, see [App Options](/docs/apps/app-options) | | `blueprint` | — | `manifest_id` of a blueprint to install alongside the app | | `assets.logo` | — | Local path to the app logo, uploaded on deploy | | `functions` | — | Server-side [functions](/docs/apps/functions/overview) (workflow actions and cron), see below | | `components` | ✓ | The app's [components](/docs/apps/components/overview) (may be empty) | ## The `functions` block | Field | Required | Description | |---|---|---| | `name` | ✓ | Unique kebab-case identifier (max 64 chars) — treat it as a public contract | | `type` | ✓ | `workflow` (flow builder action) or `scheduled` (cron per installation) | | `handler` | ✓ | Local path to the **built** JavaScript file — inlined as code on deploy | | `label` | — | Display name (TranslatedString), e.g. shown in the installed app's functions summary | | `description` | — | TranslatedString | | `schedule` | scheduled | 5-field cron or `rate(...)` — see [Scheduled Functions](/docs/apps/functions/scheduled-functions) | | `schedule_timezone` | — | IANA timezone for cron evaluation (default `Europe/Berlin`) | | `secrets` | — | **Deprecated and ignored** — functions now receive all [app option](/docs/apps/app-options) values in `input.app_options` automatically | A `workflow` function is wired into the flow builder by a `CUSTOM_FLOW_ACTION` component whose configuration references it — the component carries the org-facing name, options, config surface and `wait_for_callback`: ```json , "configuration": } ``` Limits: at most 10 functions per app, at most 5 of them scheduled, 300 KB code per function. ## Local-only fields Some fields exist only for the CLI and are never sent to the platform: - `_dir` on components — maps a component to its directory under `components/` - `handler` and `assets` paths — resolved and uploaded/inlined at deploy time - **Option values** are never in the manifest — the manifest declares the [options](/docs/apps/app-options); installing orgs enter the values per installation ## Golden rules 1. **The manifest wins.** Deploy is a sync, not a merge — what's not declared gets removed from the version. 2. **Don't hand-edit `app_id`** or component `id`s of deployed components; they are identity. 3. **`description.de` is always required** — the platform's primary market is German-speaking. 4. Validate before deploying: `npx @epilot/cli app validate` runs the same checks as the platform. --- # App Options App Options are the settings an organization fills in when installing your app — API URLs, feature toggles, credentials. You declare them **once, at the top level of the manifest**, and every part of your app reads from the same set of values: components, [functions](/docs/apps/functions/overview), the [API Proxy](/docs/apps/components/api-proxy) and [Portal Extension](/docs/apps/components/portal-extension) hooks. > **Info: Options used to live on components** Earlier app versions declared options per component. That model is retired: all existing component options were migrated to app level, and the platform transparently keeps serving them to consumers that still read options from components. Declare new options only at the top level — deploys that declare an option on a component with the same key as an app-level option are rejected. ## Declaring options Add a top-level `options` array to your `manifest.json`: ```json title="manifest.json" , , , ], "components": [ ... ] } ``` | Field | Required | Description | |---|---|---| | `key` | ✓ | Unique identifier across the whole app. This is what you reference everywhere (`}`, `input.app_options.api_key`, proxy auth) — treat it as a public contract | | `type` | ✓ | `text`, `number`, `boolean`, `secret` or `object` | | `label` | — | Human-readable label shown to the installing organization | | `description` | — | Help text shown below the input | | `required` | — | The app installs, but is marked *partially successful* and stays unusable until all required options are filled | | `sensitive` | — | Write-only, server-side only — see below. Always `true` for `type: secret` | | `repeatable` | — | The value becomes a list of entries (each gets a stable server-assigned `id`) | | `fields` | `object` type | Declares the sub-fields of an `object` option — primitives only, no nesting | The installing organization sees all app options in a single **App settings** card on the app's configuration page. ## Sensitive vs. non-sensitive Every option is one of two kinds — the same model most hosting platforms use for environment variables: - **Non-sensitive** (default) — readable wherever the app runs, **including the end-customer's browser**: journey blocks, portal blocks, capabilities. Use for URLs, labels, toggles, IDs. - **Sensitive** — **write-only and server-side only.** The value is never returned by any read API. The installer sees *Configured ✓* and when the value was last changed, and can replace it — but never read it back. Sensitive values only ever surface in server-side channels: API Proxy credential injection, Portal Extension / External Product Catalog hook templates, and function runs. Options of `type: secret` are stored encrypted and are always sensitive. Set `sensitive: true` on a non-secret type for values that aren't credentials but still must not reach a browser (internal endpoints, routing hints). **Rule of thumb:** if you would put it in a secret environment variable — API keys, tokens, client secrets — make it `type: secret`. If in doubt, mark it sensitive; you can't loosen a leaked value after the fact. ## Required options Marking an option `required: true` doesn't block the installation itself — the app installs, but is flagged as **partially successful** and cannot be used until the organization fills in all required values. ![Partial installation](/img/apps/component-option-partially-installed.png) ## Consuming options Where the configured values show up, by surface: | Surface | How you access options | Sensitive values? | |---|---|---| | [API Proxy](/docs/apps/components/api-proxy) | Reference option keys in the auth configuration — credentials are injected server-side | ✓ | | [Portal Extension](/docs/apps/components/portal-extension) / [External Product Catalog](/docs/apps/components/external-product-catalog) hooks | `}` template variables — resolved server-side when the hook fires | ✓ | | [Functions](/docs/apps/functions/writing-functions) | `input.app_options` contains **all** option values — functions always run server-side | ✓ | | Custom Journey Block, Custom Capability, Custom Page, Custom Portal Block (browser code) | Options are passed into your component per its surface contract | Non-sensitive only | Anything that executes in a browser only ever receives non-sensitive options. There is no way to read a sensitive value from frontend code — route those calls through the API Proxy instead, which attaches the credentials server-side. --- # App Surfaces App Surfaces are specific locations within the epilot platform where your custom app can be embedded. Each surface type provides a different context and set of capabilities, allowing you to extend epilot's functionality in targeted ways. ## Overview When you build an app for epilot, it runs inside an iframe embedded within the epilot XRM. The `@epilot/app-bridge` library enables communication between your app and the parent epilot application, providing: - **Authentication** - OAuth tokens for epilot API calls - **Localization** - Access to the user's language preference - **Context** - Entity, page, or action configuration data - **Messaging** - Two-way communication with the parent app ```bash title="Install the App Bridge" npm install @epilot/app-bridge ``` ## Available Surface Types ### Entity Capability A collapsible section within an entity detail view. Use this surface to display entity-specific data, metrics, or custom UI elements directly on the entity page. ``` ┌─────────────────────────────────────┐ │ Entity: Contact - John Doe │ ├─────────────────────────────────────┤ │ ▼ Overview │ │ Name: John Doe │ │ Email: john@example.com │ ├─────────────────────────────────────┤ │ ▼ Your App Capability ← Your App │ │ ┌─────────────────────────────┐ │ │ │ Custom content here │ │ │ │ (iframe) │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ``` **Use cases:** - Display external data related to an entity - Show metrics or analytics - Provide quick actions or tools - Integrate third-party services ### Entity Tab A dedicated tab within the entity detail view. Tabs provide more space than capabilities and are ideal for comprehensive features that require a full-page layout. ``` ┌─────────────────────────────────────┐ │ Entity: Contact - John Doe │ ├─────────────────────────────────────┤ │ Overview │ History │ Your Tab ← │ ├─────────────────────────────────────┤ │ ┌─────────────────────────────────┐ │ │ │ │ │ │ │ Your full-page content │ │ │ │ (iframe) │ │ │ │ │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────┘ ``` **Use cases:** - Complex data visualizations - Full-featured integrations - Document management - Detailed analytics dashboards ### Flow Action Config Configuration UI for custom automation actions. When users add your custom action to an automation workflow, this surface displays your configuration interface. ``` ┌─────────────────────────────────────┐ │ Automation: New Order Processing │ ├─────────────────────────────────────┤ │ Trigger: Order Created │ │ ↓ │ │ Action: Your Custom Action │ │ ┌─────────────────────────────────┐ │ │ │ Configuration: │ │ │ │ Webhook URL: [____________] │ │ │ │ Enable: [✓] │ │ │ │ (iframe) │ │ │ └─────────────────────────────────┘ │ │ ↓ │ │ Action: Send Email │ └─────────────────────────────────────┘ ``` **Use cases:** - Configure webhook endpoints - Set up third-party service credentials - Define action parameters - Map data fields ### Custom Page A full standalone page within the epilot 360 portal. Custom Pages register their own `/app/` route, use the standard 360 layout (sidebar navigation + topbar), and appear as navigation items in the **Custom** workplace. They support sub-pages and deep-linking. **Use cases:** - Data dashboards and explorers - Admin panels and settings pages - Multi-step wizards or onboarding flows - Full-page third-party integrations **See also:** [Custom Page component docs](/docs/apps/components/custom-page) for component configuration details. --- ## Getting Started with App Bridge ### Basic Initialization > **Tip: Every app **must** call `initialize()` before using any other app-bridge functions. This establishes the communication channel with the parent epilot application.** ```typescript title="Basic initialization" async function main() = await initialize(); // token: OAuth access token for epilot APIs // lang: User's language preference ('en', 'de', etc.) console.log('App initialized with language:', lang); } main().catch(console.error); ``` ### Initialization Options ```typescript title="Initialization with options" const session = await initialize(); ``` --- ## Entity Surface Implementation For both Entity Capability and Entity Tab surfaces, use the entity-related APIs. ### Getting Entity Context ```typescript title="Getting entity context" async function main() = await initialize(); // Get the entity being viewed const context = await getEntityContext(); console.log('Entity ID:', context.entityId); console.log('Schema:', context.schema); // e.g., 'contact', 'order' console.log('Capability:', context.capability); // Your app's capability config } ``` **Context Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `entityId` | `string` | UUID of the entity being viewed | | `schema` | `string` | Entity schema slug (e.g., `'contact'`, `'order'`, `'opportunity'`) | | `capability` | `EntityCapability` | Capability configuration from your app manifest | | `isVisible` | `boolean` | Whether the tab/capability is currently visible (tabs only) | ### Dynamic Content Height Since your app runs in an iframe, you need to communicate your content height to epilot so it can adjust the iframe size appropriately. ```typescript title="Report content height" async function main() ``` **For dynamic content, use ResizeObserver:** ```typescript title="Auto-update height with ResizeObserver" async function main() ); observer.observe(container); } ``` ### Handling Tab Visibility For Entity Tab surfaces, the tab may be hidden when the user switches to another tab. Handle visibility changes to optimize performance: ```typescript title="Handle tab visibility changes" import from '@epilot/app-bridge'; async function main() = await getEntityContext(); // Initial load only if visible if (isVisible) // Subscribe to visibility changes const unsubscribe = onVisibilityChange((visible) => }); // Clean up when your app unmounts // unsubscribe(); } ``` --- ## Action Config Implementation For Flow Action Config surfaces, use the action configuration APIs. ### Reading Action Configuration ```typescript title="Read action config" // Define your configuration type interface WebhookConfig async function main() ); } ``` **Config Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `custom_action_config` | `T` | Your custom configuration object | | `description` | `string?` | Action description shown in the UI | | `app_id` | `string?` | Associated app ID | ### Updating Action Configuration When the user changes configuration in your UI, update it immediately: ```typescript title="Update action config on change" interface WebhookConfig async function main() = await getActionConfig(); // Set up form with existing values const urlInput = document.getElementById('webhook-url') as HTMLInputElement; urlInput.value = custom_action_config?.url ?? ''; // Update config when user makes changes urlInput.addEventListener('change', () => ); }); } ``` ### Async Actions with Callbacks If your action performs asynchronous work and the automation should wait for it to complete, use `waitForCallback`: ```typescript title="Enable async callback" updateActionConfig( , ); ``` When `waitForCallback` is true, the automation engine will wait for your action to signal completion before proceeding to the next step. --- ## Page Surface Implementation For Custom Page surfaces, use the page-related APIs. These APIs let you read the current page context, navigate between sub-pages, and respond to browser back/forward navigation. ### Getting Page Context ```typescript title="Get page context" async function main() = await initialize(); // Get the page context const = await getPageContext(); console.log('Page slug:', slug); // e.g., "energy-prices" console.log('Sub-path:', subPath); // e.g., "/dashboard" console.log('Full path:', path); // e.g., "/app/energy-prices/dashboard" // Render based on current sub-path renderPage(subPath); } ``` **Context Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `slug` | `string` | The page slug from the URL (e.g., `'energy-prices'`) | | `subPath` | `string` | Sub-path after the slug (e.g., `'/dashboard'`, `'/settings/advanced'`) | | `path` | `string` | Full URL path (e.g., `'/app/energy-prices/dashboard'`) | ### Sub-Page Navigation Use `navigate()` to change the parent frame URL when users interact with your app's internal navigation. This enables deep-linking and updates the browser address bar. ```typescript title="Navigate between sub-pages" // Navigate to sub-pages within your app navigate('/dashboard'); // URL becomes /app//dashboard navigate('/settings/advanced'); // URL becomes /app//settings/advanced navigate('/'); // URL becomes /app/ ``` > **Tip: `navigate()` triggers a `history.pushState` in the parent frame. It does not reload the page or the iframe - your app stays mounted. Update your UI in response to the navigation.** ### Handling Browser Back/Forward When the user clicks the browser back or forward button, the parent frame detects the `popstate` event and notifies your app: ```typescript title="React to browser navigation" const unsubscribe = onLocationChange((subPath) => ); // Clean up when your app unmounts // unsubscribe(); ``` ### Combining Navigate and Location Change A typical pattern is to use `navigate()` for user-initiated navigation and `onLocationChange()` for browser-initiated navigation: ```typescript title="Full navigation pattern" import from '@epilot/app-bridge'; async function main() = await getPageContext(); // Initial render renderPage(subPath); // Handle browser back/forward onLocationChange((newSubPath) => ); // Handle user clicks on your internal nav document.addEventListener('click', (e) => }); } function renderPage(subPath: string) ``` --- ## Authorizing API Clients The app-bridge provides a convenient way to authorize epilot SDK clients: ```typescript title="Authorize SDK clients" async function main() ); } ``` You can also pass just the token string: ```typescript authorizeClient(entityClient, session.token); ``` --- ## Custom Events For advanced use cases, you can send and receive custom events: ### Sending Events ```typescript // Send a custom event to the parent send('my-custom-event', , }); ``` ### Receiving Events ```typescript // Listen for custom events const unsubscribe = on<>('parent-event', (data) => ); // Cleanup when done unsubscribe(); ``` ### Wildcard Subscriptions ```typescript // Listen to all events on('*', (data) => ); // Listen to events with a prefix on('custom-*', (data) => ); ``` --- ## Error Handling The app-bridge provides specific error types for better error handling: ```typescript import from '@epilot/app-bridge'; // Handle initialization timeout try ); } catch (error) ms`); showError('Failed to connect to epilot. Please refresh the page.'); } } // Handle not initialized error try catch (error) } ``` --- ## Complete Examples ### Entity Capability App A complete example of an app that displays external CRM data for a contact: ```typescript title="Entity Capability - CRM integration example" import from '@epilot/app-bridge'; interface ExternalCRMData async function main() = await initialize(); // 2. Set up API client const entityClient = getClient(); authorizeClient(entityClient, token); // 3. Set language document.documentElement.lang = lang; // 4. Get entity context const = await getEntityContext(); // 5. Fetch entity data const entity = await entityClient.getEntity(); // 6. Fetch external CRM data (your API) const externalData = await fetchExternalCRM(entity.data.email); // 7. Render UI renderCRMWidget(externalData); // 8. Update content height const observer = new ResizeObserver((entries) => ); observer.observe(document.getElementById('app')!); } function renderCRMWidget(data: ExternalCRMData)
Last Contact: $
$

`).join('')}
`; } async function fetchExternalCRM(email: string): Promise `); return response.json(); } main().catch(console.error); ``` ### Entity Tab with Visibility Handling An app that displays a dashboard, refreshing data when the tab becomes visible: ```typescript title="Entity Tab - Dashboard with visibility handling" import from '@epilot/app-bridge'; async function main() = await initialize(); const = await getEntityContext(); // Initial setup await setupDashboard(entityId, schema); // Load data if tab is already visible if (isVisible) // Refresh data when tab becomes visible onVisibilityChange(async (visible) => }); // Update height updateContentHeight(document.body.scrollHeight); } async function setupDashboard(entityId: string, schema: string) async function loadDashboardData(entityId: string) main().catch(console.error); ``` ### Automation Action Configuration A complete configuration UI for a webhook action: ```typescript title="Flow Action Config - Webhook configuration UI" import from '@epilot/app-bridge'; interface WebhookActionConfig async function main() = await getActionConfig(); const config = custom_action_config ?? , retryCount: 3, }; // Render configuration form renderForm(config); updateContentHeight(document.body.scrollHeight); } function renderForm(config: WebhookActionConfig) " required />
`; // Listen for changes const form = document.getElementById('config-form')!; form.addEventListener('change', () => ; updateActionConfig(newConfig); }); } main().catch(console.error); ``` ### Custom Page App A complete example of a custom page app with sub-page navigation: ```typescript title="Custom Page - Multi-page app with navigation" import from '@epilot/app-bridge'; type Route = '/' | '/dashboard' | '/settings'; async function main() = await initialize(); // 2. Set up API client const entityClient = getClient(); authorizeClient(entityClient, token); // 3. Get page context const = await getPageContext(); // 4. Render initial page renderPage(subPath as Route); // 5. Handle browser back/forward onLocationChange((newSubPath) => ); // 6. Set up internal navigation setupNavigation(); } function setupNavigation() }); } function renderPage(route: Route) >Home Dashboard Settings `; switch (route) updateContentHeight(document.body.scrollHeight); } main().catch(console.error); ``` > **Tip: Sample App** For a complete, production-ready example of a Custom Page app, see the **Energy Spot Price Explorer** sample: [github.com/epilot-dev/epilot-app-sample-energy-prices](https://github.com/epilot-dev/epilot-app-sample-energy-prices) --- ## API Reference ### Session Management | Function | Returns | Description | |----------|---------|-------------| | `initialize(options?)` | `Promise` | Initialize the app bridge and establish the postMessage channel with the parent epilot app. Returns session data with an OAuth token and the user's language. Safe to call multiple times - subsequent calls return the cached session. | | `getSession()` | `AppBridgeSession` | Get the cached session synchronously. Throws `AppBridgeNotInitializedError` if called before `initialize()`. | | `isInitialized()` | `boolean` | Check whether the app bridge has been initialized. | **InitOptions:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `contentHeight` | `number` | `document.body.scrollHeight` | Initial iframe height in pixels to report to the parent | | `timeout` | `number` | `5000` | Timeout in milliseconds for the initialization handshake | ### Entity Surface API | Function | Returns | Description | |----------|---------|-------------| | `getEntityContext(options?)` | `Promise` | Get the entity being viewed. Available on Entity Capability and Entity Tab surfaces. | | `updateContentHeight(height)` | `void` | Report your content height to the parent so it can resize the iframe. Call after rendering or when content size changes. | | `onVisibilityChange(handler)` | `Unsubscribe` | Subscribe to visibility changes on Entity Tab surfaces. The handler receives `true` when the tab becomes visible, `false` when hidden. Returns an unsubscribe function. | ### Action Config API | Function | Returns | Description | |----------|---------|-------------| | `getActionConfig(options?)` | `Promise>` | Get the current action configuration. Available on Flow Action Config surfaces. | | `updateActionConfig(config, options?)` | `void` | Update the action configuration. The parent automation UI receives the new config immediately. | **UpdateConfigOptions:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `waitForCallback` | `boolean` | `false` | If true, the automation engine waits for an async callback before proceeding to the next action | ### Page Surface API | Function | Returns | Description | |----------|---------|-------------| | `getPageContext(options?)` | `Promise` | Get the page context including slug, sub-path, and full path. Available on Custom Page surfaces. | | `navigate(subPath)` | `void` | Navigate to a sub-path within the current page. Triggers `history.pushState` in the parent frame, updating the browser URL. Does not reload the iframe. | | `onLocationChange(handler)` | `Unsubscribe` | Subscribe to browser back/forward navigation. The handler receives the new sub-path. Returns an unsubscribe function. | ### Generic Event API | Function | Returns | Description | |----------|---------|-------------| | `on(event, handler)` | `Unsubscribe` | Subscribe to events from the parent app. Supports wildcard patterns (e.g., `'custom-*'` or `'*'`). Returns an unsubscribe function. | | `send(event, data?)` | `void` | Send a custom event to the parent app. Use for custom communication not covered by the high-level APIs. | ### Client Authorization | Function | Returns | Description | |----------|---------|-------------| | `authorizeClient(client, sessionOrToken)` | `void` | Authorize an `@epilot/*-client` SDK client with the session token. Accepts either an `AppBridgeSession` object or a token string. Sets the `Authorization: Bearer ` header on the client. | ### Low-Level Messaging API For advanced use cases, the `epilot` object provides direct access to the postMessage transport: ```typescript // Send a raw message to the parent epilot.sendMessageToParent('my-event', ); // Subscribe to raw messages from the parent (supports wildcards) const unsubscribe = epilot.subscribeToParentMessages('my-event', (data) => ); ``` | Function | Returns | Description | |----------|---------|-------------| | `epilot.sendMessageToParent(event, detail?)` | `void` | Send a raw postMessage to the parent window | | `epilot.subscribeToParentMessages(event, handler)` | `Unsubscribe` | Subscribe to raw postMessage events from the parent. Supports wildcard matching with `*`. | ### Types ```typescript import type InitOptions, // RequestOptions, // // Entity Surface EntityContext, // EntityCapability, // // Page Surface PageContext, // // Action Config Surface ActionConfig, // UpdateConfigOptions, // // Event Handlers MessageHandler, // (data: T) => void VisibilityHandler, // (isVisible: boolean) => void Unsubscribe, // () => void } from '@epilot/app-bridge'; ``` ### Error Types ```typescript import from '@epilot/app-bridge'; ``` | Error | Properties | Description | |-------|------------|-------------| | `AppBridgeError` | `message` | Base error class. All app-bridge errors extend this. | | `AppBridgeTimeoutError` | `event: string`, `timeout: number` | A request/response operation timed out. The `event` property identifies which operation failed and `timeout` is the duration in ms. | | `AppBridgeNotInitializedError` | - | `getSession()` or other functions were called before `initialize()`. | --- ## Best Practices > **Caution: Always wrap `initialize()` in a try-catch block. If the app bridge fails to connect (e.g., the app is loaded outside of epilot), your app should display a user-friendly error message rather than silently failing.** 1. **Always initialize first** - Call `initialize()` before any other app-bridge functions 2. **Handle errors gracefully** - Wrap initialization in try-catch and show user-friendly errors 3. **Update content height** - Always report your content height, especially after rendering 4. **Use ResizeObserver** - For dynamic content, observe size changes automatically 5. **Handle visibility** - For tabs, pause expensive operations when not visible 6. **Type your configs** - Use TypeScript generics for type-safe configuration 7. **Clean up subscriptions** - Call unsubscribe functions when your app unmounts --- # The App CLI The epilot CLI is the primary way to build, validate and deploy apps. Everything it does is driven by your [`manifest.json`](/docs/apps/app-manifest) — commands scaffold code, keep the manifest in sync, and push the result to the platform. ```bash # no install needed npx @epilot/cli app # or globally npm install -g @epilot/cli ``` ## Authentication ```bash epilot auth login # browser-based login epilot auth login --token … # or paste a token ``` Every `app` command also accepts `--token` / the `EPILOT_TOKEN` env var, and `--profile` for switching between accounts. Use `--server https://app.dev.sls.epilot.io` to target a non-production environment. ## Commands ### `epilot app init ` Scaffolds a new app project: a monorepo with `manifest.json`, `components/`, Turborepo config and a `SKILL.md` that teaches AI coding agents how to work with the project. ### `epilot app add-component --type ` Adds a component package under `components//` from a template and registers it in the manifest. Types: `CUSTOM_CAPABILITY`, `CUSTOM_PAGE`, `CUSTOM_JOURNEY_BLOCK`, `CUSTOM_PORTAL_BLOCK`, `CUSTOM_FLOW_ACTION_FUNCTION` (flow action running a workflow function), `CUSTOM_FLOW_ACTION_EXTERNAL` (webhook), `PORTAL_EXTENSION`, `EXTERNAL_PRODUCT_CATALOG`, `API_PROXY`. ### `epilot app add-function [--type workflow|scheduled] [--schedule ""] [--label ""]` Adds a server-side [function](/docs/apps/functions/overview) under `functions//` and registers it in the manifest. `--schedule` implies `--type scheduled`. Workflow functions are wired into the flow builder via a `CUSTOM_FLOW_ACTION` component that references them (`add-component … --type CUSTOM_FLOW_ACTION_FUNCTION`). ```bash epilot app add-function reserve-slot --type workflow epilot app add-function nightly-sync --schedule "0 3 * * *" ``` ### `epilot app remove-component ` Removes a component from the manifest (and optionally its directory). ### `epilot app validate` Validates `manifest.json` locally with the same rules the platform enforces on deploy — schema shape, function types, schedule expressions (minimum interval, format), counts. Run it in CI. ### `epilot app deploy [--dry-run] [--new-version]` The sync command. It: 1. creates the app on first deploy (writes `app_id` back into the manifest), 2. updates metadata, permissions and blueprint references, 3. inlines each function's built `handler` as code and uploads function config UIs, 4. uploads component bundles/zips and upserts all components, 5. deletes remote components/functions that are no longer in the manifest, 6. creates a new version automatically when the latest one is published (published versions are immutable), 7. re-syncs your own org's installation if the app is installed there. `--dry-run` prints what would change without touching anything. ### `epilot app dev` Local development loop — serve components locally and preview them in epilot via [development mode](/docs/apps/development-mode) without deploying. ### `epilot app export` Reconstructs a local `manifest.json` from a deployed app — useful for migrating an app that was originally created in the UI to the CLI workflow. ### `epilot app versions` / `epilot app review` List an app's versions, and submit a version for [marketplace review](/docs/apps/publishing/verification-process). ### `epilot app api ` Escape hatch: call any App API operation directly (`epilot app api getInstallation`, …) with your CLI credentials. ## Recommended workflow ```bash epilot app init my-app && cd my-app epilot app add-component my-tab --type CUSTOM_CAPABILITY epilot app add-function nightly-sync --schedule "0 3 * * *" npm install && npm run build epilot app validate epilot app deploy ``` Commit the whole project — manifest and code together are the reproducible definition of your app. The UI's app builder remains available for exploring, but repository + CLI is the recommended path for anything you intend to maintain. --- # API Proxy API Proxy components let your app call external APIs without exposing credentials to the browser. The proxy runs server-side — API keys, OAuth tokens, and secrets never reach the client. This is especially useful for **client-facing components** like journey blocks, portal blocks, and capabilities that need to call external APIs. ## How it works ``` Browser (your app component) │ │ proxy('my-api', '/products', ) │ ▼ epilot Proxy (server-side) │ ✓ Injects auth credentials │ ✓ Signs request with Ed25519 │ ✓ SSRF protection │ ▼ External API (https://api.example.com/products) ``` ## Quick start ### 1. Add an API Proxy component to your app In the App Builder, add a new **API Proxy** component and configure: - **Name** — a unique identifier (e.g. `products-api`) - **Target URL** — the base URL of the external API (must be HTTPS) - **Auth Type** — how to authenticate with the target API ### 2. Add secret options to your app If your proxy uses authentication, declare a **secret** type [app option](/docs/apps/app-options) at the top level of your manifest. The installer provides the actual API key or credentials when installing your app; the proxy resolves the value server-side and injects it into the forwarded request — it never reaches the browser. ### 3. Call the proxy from your app ```bash npm install @epilot/app-sdk ``` ```typescript const response = await proxy( 'products-api', // proxy name (as configured in step 1) '/products', // path on the target API , } ); const data = await response.json(); ``` ## Authentication types ### None No credentials are injected. Useful for public APIs. ### Custom Header Injects a secret value as a custom header (e.g. `X-API-Key`). ``` X-API-Key: ``` ### Bearer Token Injects a secret value as a Bearer token. ``` Authorization: Bearer ``` ### OAuth 2.0 (Client Credentials) The proxy handles the full OAuth 2.0 client credentials flow: 1. Resolves `client_id` and `client_secret` from your app's options 2. Exchanges them at the token endpoint for an access token 3. Caches the token until it expires 4. Injects it as a Bearer header Configure: - **Token URL** — the OAuth token endpoint (e.g. `https://auth.example.com/oauth/token`) - **Client ID** — reference to an app option - **Client Secret** — reference to a secret-type app option - **Scope** — optional, reference to an app option ## Request signing Every proxy request is signed with Ed25519 so the target API can verify it came from epilot. Three headers are added to every forwarded request: | Header | Example | | --- | --- | | `webhook-id` | `msg_a1b2c3d4e5f6...` | | `webhook-timestamp` | `1711360000` | | `webhook-signature` | `v1a,base64...` | The signed content is `..`. ### Verifying signatures with the App SDK The `@epilot/app-sdk` provides a `verifyEpilotSignature` helper to verify incoming requests: ```typescript // Express / Node.js example app.post('/webhook', async (req, res) => ); } // Process the verified request res.json(); }); ``` The SDK automatically fetches and caches the public key from `https://cdn.app.sls.epilot.io/v1/.well-known/public-key`. > **Tip: Make sure the raw request body is available as `req.body` (string or Buffer). If you're using a JSON body parser, configure it to also preserve the raw body — the signature is verified against the raw body, not the parsed object.** ### Manual verification If you prefer to verify manually, fetch the public key and use Ed25519: ```bash curl https://cdn.app.sls.epilot.io/v1/.well-known/public-key ``` ```json ``` ```typescript const signedContent = `$.$.$`; const isValid = verify( null, Buffer.from(signedContent, 'utf8'), publicKeyPem, Buffer.from(signature, 'base64') ); ``` ## Security - Secrets are **encrypted at rest** with KMS — never stored in plaintext - Secret values are **never returned** to client-side components - Only **HTTPS** targets are allowed - **SSRF protection** validates target URLs before forwarding - Requests require a valid epilot auth token and an active app installation --- # Custom Workflow Action Extend epilot's automation capabilities with custom actions that integrate seamlessly into the platform's workflow engine. ## What Are Custom Actions? epilot provides a powerful automation engine for creating complex workflows and processes. Custom Actions are specialized components that you add to these workflows, enabling integrations and processing steps that are not available out-of-the-box. Customers install these actions to extend their automation capabilities with tailored solutions that fit their business needs. ## Why Use Custom Actions? Custom Actions provide several key benefits: - **Tailored Functionality**: Create specific actions that address unique business requirements - **Seamless Integration**: Actions fit naturally into epilot's automation workflows, enhancing existing processes - **Reusability**: Once created, actions can be reused across multiple workflows, reducing the time to set up new automations - **Community Sharing**: Share your custom actions with the epilot community, allowing others to benefit from your innovations ## How to Create a Custom Action > **Tip: Run code inside epilot instead?** If you want epilot to run your JavaScript for you — no infrastructure on your side — keep the flow action component but point its configuration at a [workflow function](/docs/apps/functions/workflow-functions): ``. The component keeps carrying the name, options and config UI; the function carries the code. ## External Integration ### How To Integrate External Systems A custom action with an external integration is basically a webhook (POST) request to your defined system. You can either: 1. Trigger some asynchronous processing in your system, or 2. Update epilot data with an access token built from the permission/role you can define (see [Permissions](/docs/apps/configure-permissions)). The payload of the request is the following: ```json , "app_config": {}, "app_options": {}, // The options configured by the user when installing the app "execution_id": "", "execution_status": "", "exection_action_id": "", "trigger_event": "", // information about the calling event, e.g. "opportunity.created", "opportunity.updated", etc. } } ``` Additionally, you will receive 4 headers specified by epilot: ```http webhook-signature v1a,puLZGVBm1MhSFz/kpgDsbt56DqanAAEg5Y5pgkVaz2d9WTbp6sGpo64qJFm8DWE8fo85b3cOs0CvV9v4WseUBw== webhook-timestamp 1749649159 webhook-id msg_245c477923600038edb96d07f9d95f77 optional: x-epilot-token: ey... ``` The `webhook-*` headers are used to verify and check the authenticity of the request. See more about [how to secure external integrations](#how-external-integrations-are-secured-asymmetric-signature) below. The `x-epilot-token` is an access token that is generated based on the permissions and roles defined in your app configuration. This token allows you to securely access epilot data and perform actions on behalf of the user. (expires after 10 min) ### Example: Sync Data With Your Platform and Write Back to epilot Entities To create a custom action that syncs data with your platform and writes back to epilot entities, you can follow these steps: 1. **Define the Action**: Create a new custom action in your app configuration with the type `external_integration`. 2. **Define the Permissions**: Specify the permissions required for the action to access the necessary data in epilot. Now whenever a customer installs your App and invokes the custom action, the request will be sent to your external system with the payload described above. Your system can then process the request, perform the necessary operations (e.g., syncing data), and respond accordingly. By specifying the previous permission an access token is attached to the request. You can use that token and our [official SDK](https://github.com/epilot-dev/sdk-js) to write back to epilot entities. ### How External Integrations Are Secured (Asymmetric Signature) To ensure that external integrations are secure, epilot uses an asymmetric signature mechanism. This involves generating a unique signature for each request that is sent to the external system. The signature is created using a private key that is known only to the epilot platform, and it is verified by the external system using a corresponding [public key](https://app.sls.epilot.io/v1/public/.well-known/public-key). This ensures that only authorized requests are processed, and it prevents unauthorized access to the external system. To secure the endpoint our custom action is calling, you need to verify the signature of the request. We recommend to use the `verifyEpilotSignature` function by our App SDK as it handles the verification process for you. This function checks the signature against the public key and ensures that the request is valid. We use a standard way to sign & verify the requests according to the [webhook spec](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md). --- # Custom Journey Blocks Create custom interactive components for epilot's Journey Builder ## What Are Journey Blocks? Journey Blocks are the interactive components that make up customer journeys within the epilot platform. They're the building blocks that allow businesses to create seamless, engaging customer experiences from initial contact through completion of a process. Custom Journey Blocks extend this capability, allowing developers to create specialized components that integrate seamlessly with epilot's Journey Builder. These custom blocks appear alongside native blocks in the Journey Builder palette and can be placed anywhere in a customer journey flow. ## Why Create Custom Journey Blocks? Custom Journey Blocks enable you to: - **Fill Functional Gaps**: Create specialized functionality not available in standard blocks - **Integrate External Systems**: Connect epilot journeys with third-party services and APIs - **Implement Business Logic**: Add industry-specific calculations and validations - **Customize UI/UX**: Design tailored experiences for specific customer segments - **Visualize Data**: Present complex information in intuitive, interactive ways ## Building Your First Journey Block Custom Journey Blocks are web components that follow standard web technologies, making them accessible to any web developer. ### Prerequisites - Basic knowledge of HTML, CSS, and JavaScript - Familiarity with Web Components standards - A development environment with Node.js ### Getting Started The fastest way to scaffold a journey block is with the **epilot CLI**: ```bash title="Scaffold a journey block with the CLI" # Initialize a new app (if you don't have one yet) npx epilot app init my-app cd my-app # Add a custom journey block component npx epilot app add-component my-block --type CUSTOM_JOURNEY_BLOCK # Install dependencies npm install # Start the development server npm run dev ``` This creates a ready-to-use component with all the boilerplate wired up, including the web component wrapper and manifest configuration. You can also find starter templates for each framework in the [app-templates](https://github.com/epilot-dev/app-templates) repository. ### Technology Recommendations > **Tip: While Journey Blocks can be built with any framework that compiles to web components, we recommend **React** for the best developer experience and access to epilot's Concorde UI library.** React offers several key advantages: - **Concorde UI Library Access**: Leverage epilot's Concorde component library for consistent styling - **Alignment with epilot's Theme**: Ensure your blocks visually match the Journey Builder interface - **Component Reusability**: Build with the same components used throughout the epilot platform - **Developer Experience**: Benefit from React's robust ecosystem and developer tools Using React with a web component wrapper gives you the best of both worlds: modern development experience and seamless integration with the Journey Builder. ### Component Mapping A powerful feature of Journey Blocks is entity mapping, which lets you: - **Connect UI Elements to Data**: Link form fields and components to specific entity properties - **Enable Data Persistence**: Store and retrieve information across journey steps - **Facilitate Data Processing**: Allow journey automation to use the collected data Component mapping is defined in your app configuration and establishes the relationship between your UI elements and epilot's entity model. This mapping ensures that data flows properly between your custom block and the rest of the journey. Example mapping types: - `string`: For text fields and standard inputs - `boolean`: For checkboxes and toggle switches - `date`: For date picker components - `datetime`: For date and time selection components - `link`: For link components ### Component Arguments ![Component Arguments](/img/apps/journey-component-args.png) Journey Blocks can be made configurable through component arguments, which allow: - **Per-Instance Configuration**: Journey creators can customize each instance of your block - **Reusable Components**: The same block can be configured differently in various parts of a journey - **User-Friendly Setup**: Non-technical users can adjust block behavior without coding Arguments are defined in your app configuration and appear in the block settings panel when a user adds your block to a journey. They can include: - Text inputs for customizable labels and messages (`text`) - Boolean toggles for feature enabling/disabling (`boolean`) - Dropdown selectors for pre-defined options (`enum`) - References to other blocks in the same journey (`block_reference`) This configurability makes your custom blocks more versatile and valuable across different use cases. #### Block reference arguments Use `block_reference` when your block needs to read or subscribe to the value of **another block in the same journey** (e.g. a Subsidy Finder that needs the zip code from an Availability Check block). Instead of asking the configuring user to copy and paste a block ID, the journey builder shows them a dropdown of compatible blocks; the chosen block's ID is stored as the arg value. ```json title="Manifest: declare a block_reference arg" , "required": true, "allowed_types": ["availability-check"] } ``` `allowed_types` is optional. When provided, the picker is filtered to those journey block types (e.g. `availability-check`, `address`, `text-input`); omit it to allow any block. At runtime your bundle receives the chosen block's ID through `container.args` exactly like any other arg, then uses the existing `subscribe` / `getValue` API to read its value: ```typescript title="Consume the block_reference at runtime" const args = JSON.parse(props.container.args || "{}"); const zipBlockId = args.zip_source; useEffect(() => ); return () => unsubscribe(); }, [zipBlockId, props.container.subscribe]); ``` > **Tip: Prefer `block_reference` over a plain `text` arg whenever your block needs another block's ID — it removes the manual copy/paste step for the configuring user and prevents typos.** ## Best Practices ### Performance Considerations - **Bundle Size**: Keep your bundle under 500KB to ensure quick loading times - **Lazy Loading**: Load external resources only when needed - **Asset Optimization**: Compress images and minimize CSS/JS - **Efficient DOM Operations**: Minimize DOM manipulations and reflows ### Bundling Your Component > **Caution: Your component must be bundled into a **single `bundle.js` file**. This is the only format currently supported. The bundle must include all styles and assets inline.** ### UI Design Guidelines For consistent user experience, your custom blocks should: - Follow epilot's design language - Be responsive and accessible - Provide clear feedback on actions - Include proper validation and error states ## Testing Your Journey Block Before submitting your block: 1. Test in different browsers (Chrome, Firefox, Safari) 2. Verify responsiveness on different screen sizes 3. Ensure accessibility standards are met 4. Check for memory leaks during repeated use ## Journey Block Usage Once your App with a custom Journey Block is installed, you can use it in the Journey Builder and configure it according to your needs: ![Block Usage](/img/apps/component-journey-installed.png) ## Calling External APIs via Proxy Custom journey blocks run in the browser. If your block needs to call a third-party API that requires credentials (API keys, OAuth tokens, etc.), **never embed those secrets in your client-side code**. Instead, use the [API Proxy](./api-proxy.md) — it runs server-side and injects credentials on your behalf so they never reach the browser. ### Setup 1. **Add an API Proxy component** to your app in the App Builder (see [API Proxy docs](./api-proxy.md) for full setup). 2. **Install the App SDK** in your journey block project: ```bash npm install @epilot/app-sdk ``` ### Using the proxy in a journey block Journey blocks receive a `publicToken` via the container props. Pass this token to the `proxy` function to authenticate the request: ```typescript function App(props: AppProps) , }) .then((response) => response.json()) .then((data) => console.log('Proxy response:', data)) .catch((error) => console.error('Proxy error:', error)); }, [props.container.publicToken]); return
...
; } ``` | Parameter | Description | | --- | --- | | `'my-api'` | The proxy name you configured in the App Builder | | `'/endpoint'` | The path appended to the proxy's target URL | | `appId` | Your app's ID | | `token` | The `publicToken` from the journey block container props | | `method` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.) | | `body` | Optional request body (automatically serialized to JSON) | > **Tip: The `publicToken` is only available at runtime when the journey is rendered for an end user. During development, you can test with a hardcoded token — just make sure to remove it before publishing.** ### How it works 1. Your journey block calls `proxy()` with the `publicToken` 2. The request is sent to the epilot proxy server (not directly to the third-party API) 3. The proxy resolves the credentials configured in the App Builder (API key, Bearer token, or OAuth 2.0) 4. The proxy forwards the request to the target API with credentials injected server-side 5. The response is returned to your journey block This means your API keys and secrets are **never exposed** in the journey's client-side bundle or network requests visible to end users. For full details on authentication types, request signing, and security, see the [API Proxy documentation](./api-proxy.md). ## Useful Resources ### Journey UI Library Concorde (React) While custom blocks are framework-agnostic, you can reference epilot's UI components for design consistency: - [Source Code](https://github.com/epilot-dev/concorde-elements) - [Storybook](https://portal.epilot.cloud/concorde-elements) ### Documentation and Examples - [Web Components MDN Guide](https://developer.mozilla.org/en-US/docs/Web/Web_Components) --- # Custom Page Add full, standalone pages to the epilot 360 portal with your own routes, navigation items, and sub-page support. ## What Are Custom Pages? Custom Pages allow your app to register entirely new routes within the epilot 360 portal. Unlike other component types that embed within existing views, Custom Pages give your app a dedicated full-page surface with the standard 360 layout (sidebar navigation + topbar). Users access your pages via navigation items that appear automatically in the **Custom** workplace. ## Why Use Custom Pages? Custom Pages are ideal when your app needs more than a widget or sidebar section: - **Full-page experiences** - Dashboards, data explorers, configuration panels, or any UI that needs the full viewport - **Multi-page apps** - Support sub-pages and deep-linking (e.g., `/app/energy-prices/dashboard`, `/app/energy-prices/settings`) - **Native feel** - Pages use the standard 360 layout and appear in the sidebar navigation alongside built-in features - **Browser navigation** - Full support for back/forward buttons and URL sharing via deep-linking ## How It Works 1. You register a `CUSTOM_PAGE` component in your app manifest with a unique **slug** and navigation metadata 2. When a user installs your app, the page appears as a navigation item in the **Custom** workplace 3. Visiting the page loads your app in an iframe at `/app/` 4. Your app communicates with the 360 portal via the `@epilot/app-bridge` library ## Creating a Custom Page Component ### Component Configuration Add a `CUSTOM_PAGE` component to your app manifest: ```json } } ``` ### Configuration Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `slug` | string | Yes | URL slug for the page route. Must be lowercase alphanumeric with hyphens, 2+ characters. Must not conflict with built-in routes. | | `config.nav_label` | string | Yes | Label shown in the sidebar navigation | | `config.nav_icon` | string | No | Icon name for the navigation item | | `config.nav_description` | string | No | Short description shown in the navigation tooltip | | `config.source.url` | string | Yes | URL of your app's page UI | ### Slug Rules - Must match the pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (at least 2 characters) - Must not conflict with reserved built-in route segments (e.g., `entity`, `portal`, `automation`, etc.) - Must be unique across all installed apps in an organization ### Multiple Pages Per App A single app can register multiple `CUSTOM_PAGE` components, each with a different slug: ```json } }, } } ] } ``` ## Implementing Your Page ### Basic Setup ```typescript async function main() = await initialize(); // 2. Get the page context const = await getPageContext(); console.log('Page slug:', slug); // e.g., "energy-prices" console.log('Sub-path:', subPath); // e.g., "/dashboard" console.log('Full path:', path); // e.g., "/app/energy-prices/dashboard" // 3. Render your page based on the sub-path renderPage(subPath); // 4. Report content height updateContentHeight(document.body.scrollHeight); } main().catch(console.error); ``` ### Sub-Page Navigation Use `navigate()` to change the URL when users interact with your app's internal navigation. This updates the browser URL bar, enabling deep-linking and browser history. ```typescript // Navigate to a sub-page navigate('/dashboard'); // URL becomes /app/energy-prices/dashboard navigate('/settings/advanced'); // URL becomes /app/energy-prices/settings/advanced navigate('/'); // URL becomes /app/energy-prices ``` ### Handling Browser Back/Forward When the user clicks the browser's back or forward button, the parent frame notifies your app via `onLocationChange`: ```typescript // Subscribe to browser navigation events const unsubscribe = onLocationChange((subPath) => ); // Clean up when your app unmounts // unsubscribe(); ``` ### Complete Example with Router ```typescript import from '@epilot/app-bridge'; type Route = '/' | '/dashboard' | '/settings'; async function main() = await initialize(); const = await getPageContext(); // Initial render renderPage(subPath as Route); // Handle browser back/forward onLocationChange((newSubPath) => ); // Set up internal navigation document.querySelectorAll('[data-nav]').forEach((el) => ); }); } function renderPage(route: Route) updateContentHeight(document.body.scrollHeight); } main().catch(console.error); ``` ## Permissions Custom Pages inherit permissions from the app's configured grants. Users can only access your page if they have the app installed and the appropriate permissions. See [Configure Permissions](/docs/apps/configure-permissions) for details. ## Sample App For a complete working example, see the **Energy Spot Price Explorer** sample app: [github.com/epilot-dev/epilot-app-sample-energy-prices](https://github.com/epilot-dev/epilot-app-sample-energy-prices) This sample app demonstrates: - Registering a `CUSTOM_PAGE` component with navigation metadata - Using `getPageContext()` to read the current page context - Sub-page navigation with `navigate()` and `onLocationChange()` - Fetching and displaying data from a public API (German energy market data via SMARD) - Responsive charting and data visualization --- # External Product Catalog

External Product Catalog

Integrate external product catalogs with epilot

## Configuration To add an **External Product Catalog** component, create a new App or update an existing App. All development is performed using the built-in configuration editor accessible from the component editor. The editor provides you with autocomplete and validation of the configuration. That way you can start with the example provided in this documentation and fine-tune your external product catalog without ever leaving the editor. ## Hooks Hooks allow changing or adding functionality to external product catalogs. They typically rely on your API for the execution of any necessary logic and expect a certain response. ### Supported Hooks / Catalog Types The interface supports two types of data exchange, depending on the block being used in the journey: - **Products**: Returns an array of products (`type: "products"`). - **Product Recommendations**: Returns a source product and a list of offer products (`type: "product-recommendations"`). ### Template Variables You can use template variables throughout your configuration to dynamically inject values from various sources. Template variables use the `}` syntax. #### Available Variables - **`Options.*`**: Access values from the app options configured during installation - Example: `}`, `}` - **`AuthResponse.*`**: Access data from the authentication response data - Example: `}` - Use this to extract tokens or other authentication data returned by your auth endpoint - **`Context.*`**: Access properties from the current context (e.g. Journey Context) - Example: `}`, `}` ### Example Below is an example of a configuration for the `products` and `product-recommendations` hooks, assuming a typical OAuth2 authentication flow, where the client credentials are stored in the app options. ```json title="Products hook with OAuth2 authentication" , "auth": }/auth/token", "method": "POST", "headers": }", "Content-Type": "application/x-www-form-urlencoded" }, "body": }" } }, "call": }/products", "method": "POST", "headers": }" } } }, , "auth": }/auth/token", "method": "POST", "headers": }", "Content-Type": "application/x-www-form-urlencoded" }, "body": }" } }, "call": }/product-recommendations", "method": "POST", "headers": }" } } } ] } ``` ### Security Considerations > **Caution: Never hardcode sensitive credentials in your configuration. Always store API keys and secrets as app options, which are encrypted at rest.** - **Credentials Storage**: Store sensitive credentials (like API keys) as app options rather than hardcoding them - **Token Expiration**: Ensure your authentication tokens have appropriate expiration times - **HTTPS**: Always use HTTPS URLs for authentication endpoints and redirects - **Access Control**: Implement proper authorization checks on your authentication endpoint to ensure only authorized clients can obtain tokens ## Usage in Journeys After your External Product Catalog component is configured and the app is installed, it can be used within epilot's Journeys. Currently we support seamless integration of the external product catalog into the journey via Product Blocks and Product Recommendations Blocks (beta). When a journey creator adds a **Product Block** or **Product Recommendations Block** in a journey, they can select the integration as the source for products. Only hooks that are supported for the selected block type will be available (e.g. only `products` hook for Product Block, only `product-recommendations` hook for Product Recommendations Block). This allows the journey to dynamically fetch products and pricing from your external catalog in journeys. By default, only data from preceding steps (in their defined order) is passed to the steps/blocks data context. While this works for most use cases, you can configure the data context to also include data from subsequent steps, enabling non-linear journey flows. This configuration also makes the data each integration passes to the journey more explicit, lighter and easier to manage. ## Usage in Portals After your External Product Catalog component is configured and the app is installed, it can also be used within epilot's Portals. When configuring a Portal, you can add a **Product Block** and select the integration as the source for products. Only hooks that are supported for the selected block type will be available. This allows the portal to dynamically fetch products and pricing from your external catalog. ## Integration Interface To ensure seamless communication between epilot and your external catalog, your integration must respect the [External Catalog Integration Interface documentation](https://docs.api.epilot.io/pricing-api-external-catalog). ### Specification The integration works as a request to your service endpoint with the following request and response: 1. **Request**: A payload containing the `context` of the journey, portal, or a custom one defined by you/epilot. 2. **Response**: A list of products or product recommendations in a specific format. For detailed information on the request and response schemas, please refer to the [External Catalog Integration Interface documentation](https://docs.api.epilot.io/pricing-api-external-catalog). ### Response Examples Below is an example of a response for the `product-recommendations` hook. ```json title="Product Recommendations Response Example" ], "legal_footnotes": "Price valid for new customers. Prices may vary depending on consumption and region.", "price": , "price_components": [ , "name": "Working price", "id": "price-component-1", "is_composite_price": false, "type": "recurring", "billing_period": "monthly", "tax": , "unit_amount_net_decimal": "0.35", "unit_amount_gross_decimal": "0.4165", "amount_subtotal_decimal": "35", "amount_total_decimal": "41.65", "currency": "EUR" }, , "unit_amount_net_decimal": "10", "unit_amount_gross_decimal": "11.90", "amount_subtotal_decimal": "10", "amount_total_decimal": "11.90", "currency": "EUR" } ], "total_details": ] } }, "billing_duration_amount": 24, "billing_duration_unit": "months", "notice_time_amount": 6, "notice_time_unit": "weeks", "termination_time_amount": 6, "termination_time_unit": "weeks", "renewal_duration_amount": 12, "renewal_duration_unit": "months" }, "metadata": {} }, "offers": [ ], "legal_footnotes": "Price valid for new customers. Prices may vary depending on consumption and region.", "price": , "price_components": [ , "name": "Working price", "id": "price-component-3", "is_composite_price": false, "type": "recurring", "billing_period": "monthly", "tax": , "unit_amount_net_decimal": "0.3697", "unit_amount_gross_decimal": "0.44", "amount_subtotal_decimal": "36.97", "amount_total_decimal": "44.00", "currency": "EUR" }, , "unit_amount_net_decimal": "10", "unit_amount_gross_decimal": "11.90", "amount_subtotal_decimal": "10", "amount_total_decimal": "11.90", "currency": "EUR" } ], "total_details": ] } }, "billing_duration_amount": 12, "billing_duration_unit": "months", "coupons": [ ] } }, ], "legal_footnotes": "Price valid for new customers. Prices may vary depending on consumption and region.", "price": , "price_components": [ , "name": "Working price", "id": "price-component-1", "is_composite_price": false, "type": "recurring", "billing_period": "monthly", "tax": , "unit_amount_net_decimal": "0.35", "unit_amount_gross_decimal": "0.4165", "amount_subtotal_decimal": "35", "amount_total_decimal": "41.65", "currency": "EUR" }, , "unit_amount_net_decimal": "10", "unit_amount_gross_decimal": "11.90", "amount_subtotal_decimal": "10", "amount_total_decimal": "11.90", "currency": "EUR" } ], "total_details": ] } }, "billing_duration_amount": 24, "billing_duration_unit": "months", "coupons": [ ] } } ] } ``` This allows portals and journeys to display dynamic product recommendations based on your external catalog, as shown below:
--- # Overview

 

# What Are Components? ## The Foundation of epilot Apps Components are the fundamental building blocks that make up every app in the epilot ecosystem. Think of them as specialized modules that extend epilot's functionality in specific, targeted ways. Each component type is designed to integrate with a particular part of the platform, enabling seamless extensions that feel native to users. When you build an app for epilot, you're essentially creating one or more components that work together to deliver value. A single app might contain multiple components of different types, each addressing a specific use case or integration point. ## Why Components Matter This modular approach offers several key advantages: - **Targeted Integration**: Components connect precisely where they're needed in the epilot platform - **Flexible Combinations**: Mix different component types to create comprehensive solutions - **Focused Development**: Build only what you need without unnecessary complexity - **Consistent Experience**: Users benefit from a unified interface regardless of the app source ## Available Component Types epilot currently supports these component types: ### [Custom Journey Blocks](/docs/apps/components/custom-journey-block) These web components integrate directly into epilot's Journey Builder, allowing you to create specialized elements for data collection, visualization, or process automation. Journey Blocks appear in the Journey Builder palette and can be placed anywhere in a customer journey flow. ``` Use cases: Data collection forms, calculators, visualizations, third-party integrations ``` ### [Portal Extensions](/docs/apps/components/portal-extension) Portal Extensions enhance epilot's customer and installer portals with new sections, tools, or visualizations. They can be added to dashboards or specific sections to extend the portal's functionality with custom features. ``` Use cases: Custom dashboard widgets, specialized tools, data visualizations ``` ### [External Product Catalog](/docs/apps/components/external-product-catalog) External Product Catalog components integrate third-party product catalogs into epilot. They enable Product Blocks to fetch and display products and pricing from external systems, allowing customers to browse and select items from external catalogs directly within their journey experience. ``` Use cases: Custom product catalogs, journey products, journey product recommendations ``` ### [Custom Workflow Task](/docs/apps/components/custom-action) Custom Workflow Tasks extend epilot's automation engine with your own logic. You can either call an external endpoint (webhook-style) or run TypeScript code in a sandboxed environment directly within a workflow task. ``` Use cases: Third-party integrations, data syncing, custom processing steps in workflows ``` ### [API Proxy](/docs/apps/components/api-proxy) API Proxy components let your app call external APIs without exposing credentials to the browser. The proxy runs server-side, injecting authentication and signing requests so secrets never reach the client. ``` Use cases: Secure external API calls, OAuth integrations, credential-protected endpoints ``` ## On the Horizon The epilot platform continues to evolve, with new component types planned for future releases: ### Custom Journey Design Elements Extensions to the Journey Builder's design capabilities, including: - **Email Plugins**: For customizing and enhancing email templates and functionality ## Requesting New Component Types Have an idea for a new component type? We welcome feedback from the developer community. 1. **Evaluate Your Need**: Consider if existing component types could address your use case 2. **Define the Integration Point**: Identify where in epilot your component would integrate 3. **Describe the Value**: Outline the problems it would solve for epilot users 4. **Submit a Request**: [Contact us](mailto:support@epilot.cloud) with your proposal Our product team regularly reviews component requests and prioritizes them based on community interest and platform direction. ## App Surfaces & Communication When your component runs inside epilot, it's embedded in an iframe and communicates with the parent application through the `@epilot/app-bridge` library. Different component types are displayed on different "surfaces" within epilot. Learn more about [App Surfaces](/docs/apps/app-surfaces) to understand how your app communicates with epilot and receives authentication, entity context, and configuration data. ## Building Your First Component Ready to create your own component? The fastest way to get started is with the **epilot CLI**: ```bash npx epilot app init my-app npx epilot app add-component my-block --type CUSTOM_JOURNEY_BLOCK ``` The CLI manages the full app lifecycle — from scaffolding and validation to deployment and version management. Key commands: | Command | Description | | --- | --- | | `epilot app init` | Scaffold a new app project with `manifest.json` | | `epilot app add-component` | Add a component from a starter template | | `epilot app validate` | Validate your manifest before deploying | | `epilot app deploy` | Deploy your app to epilot | | `epilot app versions` | List all versions of your app | | `epilot app review` | Submit a version for public review | | `epilot app export` | Export an existing app as `manifest.json` | See the [CLI documentation](/docs/cli/commands/app) for the full command reference. Each component type also has a ready-to-use starter template available in the [app-templates](https://github.com/epilot-dev/app-templates) repository.

Journey Blocks

Create interactive elements for epilot's Journey Builder

Start Building

Portal Extensions

Extend epilot's portals with custom functionality

Start Building

External Product Catalog

Integrate third-party product catalogs into epilot

Start Building

Custom Workflow Task

Run custom logic in epilot's automation workflows

Start Building

API Proxy

Call external APIs securely without exposing credentials

Start Building
By understanding how components work and which types are available, you can create powerful extensions that enhance the epilot platform while maintaining a seamless user experience. --- # Portal Extensions

Portal Extensions

Enhance epilot portals with custom functionality and integrations

## Configuration All development is performed using the built-in configuration editor accessible from the component editor. The editor provides you with autocomplete and validation of the configuration. That way you can start with the example provided in this documentation and fine-tune your portal extension without ever leaving the editor. ![Portal Extension configuration editor](/img/apps/portal-extensions/editor.png) ## Links The easiest way to extend the functionality of portals is to link to a third party system. This is possible using the "seamless link" concept. ### What are Seamless Links? Seamless links enable portal users to access third-party systems directly without manual authentication. When a user clicks a seamless link in the portal, epilot handles the authentication process in the background according to your configured rules, then redirects the user to the specified page in the third-party system with the necessary credentials or tokens. This provides a smooth user experience where portal users can navigate to external systems without interruption, while you maintain control over the authentication mechanism and security policies. #### How Seamless Links Work ```mermaid sequenceDiagram participant User as Portal User participant epilot as epilot Portal participant Auth as Your Auth Endpoint participant App as Third-Party System User->>epilot: Clicks seamless link epilot->>Auth: Authentication request (credentials + headers) Auth-->>epilot: Returns token/credentials epilot->>App: Redirect with token parameters App-->>User: Authenticated page displayed ``` The seamless link flow consists of two main steps: 1. **Authentication**: epilot makes a request to your authentication endpoint using the configured credentials and headers 2. **Redirect**: epilot uses the authentication response to construct a redirect URL with the necessary parameters (such as tokens) and sends the user to the target page All of this happens automatically in the background, so the portal user experiences a seamless transition to the third-party system. ### Configuration and Example Seamless links are configured in your app configuration. Each seamless link defines the authentication mechanism and the redirect behavior. The built-in component configuration editor will guide you through the available properties and their format. Here is an example hook configuration: ```json title="Seamless link configuration" , "description": , "auth": }/token", "method": "GET", "headers": }", "UID": "}" } }, "redirect": }/dashboard", "params": }", "contractId": "}" } } } ``` The configuration above adds a new option for quick actions like this: ![Seamless link used in quick action](/img/apps/portal-extensions/quick-action-seamless-link.png) ### Template Variables You can use template variables throughout your seamless link configuration to dynamically inject values from various sources. Template variables use the `}` syntax. #### Available Variables - **`Options.*`**: Access values from the app options configured during installation - Example: `}`, `}` - **`Contact.*`**: Access properties from the current portal user's contact entity - Example: `}`, `}` - **`AuthResponse.*`**: Access data from the authentication response - Example: `}`, `}` - Use this to extract tokens or other authentication data returned by your auth endpoint - **`Entity.*`**: Access properties from the current context entities - Example: `}`, `}` ### Example Use Cases 1. **Billing Dashboard**: Redirect portal users to a third-party billing system to check details of their last bill 2. **Document Management**: Provide direct access to a document repository 3. **Service Portal**: Integrate with external service management systems ### Security Considerations > **Caution: Never hardcode sensitive credentials in your configuration. Always store API keys and secrets as app options, which are encrypted at rest.** - **Credentials Storage**: Store sensitive credentials (like API keys) as app options rather than hardcoding them - **Token Expiration**: Ensure your authentication tokens have appropriate expiration times - **HTTPS**: Always use HTTPS URLs for authentication endpoints and redirects - **Access Control**: Implement proper authorization checks on your authentication endpoint to ensure only authorized portal users can obtain tokens ## Hooks Hooks allow changing or adding functionality to portals. They typically rely on your API for the execution of any necessary logic and expect a certain response. For certain functionalities, users can choose which hook is used in the portal settings drawer under Extensions. ![Enable hooks in portal settings](/img/apps/portal-extensions/enabling-hooks-in-portal-settings.png) There are currently three groups of hooks supported based on their use: - Time Series Data Retrieval - Data Existence Check/Retrieval - Data Validation ### Time Series Data Retrieval Hooks Data retrieval hooks enable portals to fetch and display time-series data from third-party systems. These hooks are used to power data visualizations and charts in the portal interface, allowing portal users to view real-time or historical data from integrated systems. #### Types of Data Retrieval Hooks Portals support three types of data retrieval hooks: 1. **Price Data Retrieval** (`priceDataRetrieval`): Fetches price information over time 2. **Consumption Data Retrieval** (`consumptionDataRetrieval`): Retrieves consumption or usage data 3. **Cost Data Retrieval** (`costDataRetrieval`): Fetches cost or billing data #### How Data Retrieval Hooks Work Data retrieval hooks follow a three-step process: 1. **Authentication**: epilot authenticates with your third-party system to obtain a token or credentials 2. **Data Retrieval**: Using the authentication token, epilot makes a request to your data API with the requested time range and interval 3. **Data Resolution**: The response is processed and extracted using the configured data path, then displayed in the portal The authentication step supports caching to reduce API calls and improve performance. The data retrieval supports configurable time intervals (e.g., hourly, daily) and automatically handles time range queries based on the portal user's selected view. #### Configuration and Example Data retrieval hooks are configured in your app configuration. Each hook defines the authentication mechanism, the data retrieval endpoint, and how to process the response. Below are examples for all three data retrieval hook types. All of these enable configuring users to add a Dynamic Tariff or Consumption block where your integration is pickable using the "Integration" dropdown. ![Data retrieval integration in Dynamic Tariff block](/img/apps/portal-extensions/data-retrieval-integration-portal-builder.png) ##### Price Data Retrieval ```json title="Price data retrieval hook" , "intervals": ["PT1H"], "auth": }/token", "method": "GET", "headers": }", "UID": "}" }, "cache": }-}", "ttl": "3600" } }, "call": }/price", "headers": }" }, "params": }", "to": "}", "interval": "}" } }, "resolved": } ``` ##### Consumption Data Retrieval ```json title="Consumption data retrieval hook" , "intervals": ["PT1H"], "auth": }/token", "method": "GET", "headers": }", "UID": "}" }, "cache": }-}", "ttl": "3600" } }, "call": }/consumption", "headers": }" }, "params": }", "to": "}", "interval": "}" } }, "resolved": } ``` ##### Cost Data Retrieval ```json title="Cost data retrieval hook" , "intervals": ["PT1H"], "auth": }/token", "method": "GET", "headers": }", "UID": "}" }, "cache": }-}", "ttl": "3600" } }, "call": }/cost", "headers": }" }, "params": }", "to": "}", "interval": "}" } }, "resolved": } ``` #### Template Variables Data retrieval hooks support the same template variables as any other hook, plus additional scope variables: #### Standard Variables - **`Options.*`**: Access values from the app options configured during installation - **`Contact.*`**: Access properties from the current portal user's contact entity - **`AuthResponse.*`**: Access data from the authentication response - **`Entity.*`**: Access data from the context entities like `Contract`. #### Scope Variables (for `call.params`) These variables are automatically provided by epilot based on the portal user's selected time range and interval: - **`Scope.from`**: Start timestamp of the requested time range (ISO 8601 format) - **`Scope.to`**: End timestamp of the requested time range (ISO 8601 format) - **`Scope.interval`**: Selected time interval (ISO 8601 duration format, e.g., `"PT1H"` for hourly) #### Best Practices - **Caching**: Use authentication token caching to reduce API load and improve response times - **Interval Support**: Support multiple intervals if your API allows it to give portal users flexibility with different data views - **Error Handling**: Ensure your API returns appropriate error responses that epilot can handle gracefully - **Time Zone Handling**: Take care when handling Time Zones and DSTs #### Returning Typed Data The consumption hook may return more than one record per timestamp by adding a `type` field to each record. This lets a single chart break a value down into multiple series — for example a household that both draws from and feeds into the grid, or a meter billed on two tariffs. ```json title="Typed consumption response (prosumer)" , , , , , ] } ``` The `type` values are free-form, but they only render meaningfully when the portal knows how to label, color and combine them. That information comes from the **Visualization Metadata** hook described below — each `type` returned by the data hook should match a `type_options[].id` returned by the metadata hook. #### Visualization Metadata Hook The `visualizationMetadata` hook returns runtime metadata describing **how** a visualization should be rendered for the current portal context (which meter, contract, etc. the user is looking at). The portal invokes it _before_ the data hook, with the same context, so the shape of the chart can vary per meter or contract — different tariff models, available intervals, or history depth. A `visualizationMetadata` hook is looked up implicitly per extension (one per extension); a data-retrieval hook does not need to reference it explicitly. The metadata response has three optional fields: - **`type_options`**: the series advertised for this context. Each option's `id` matches the `type` field on the data hook records. Options also carry a localized `label`, an `aggregation_group`, a `statistical_method`, a `unit`, a Spark `color`, and a display `precision`. - **`intervals`**: the intervals supported for this context (`PT15M`, `PT1H`, `P1D`, `P1M`). Prefer this over the now-deprecated `intervals` field on the data-retrieval hooks, so the supported intervals can vary per meter/contract. - **`data_range`**: the earliest (`from`) and latest (`to`) timestamps for which data is available, used to bound the date picker. The `statistical_method` on each type both describes the aggregation already applied to that type's data and dictates the chart shape: - `sum` → **bar chart**. Same-`aggregation_group` types are stacked into one bar; different groups render side-by-side. - `min` / `average` / `max` → **line chart**. Same-`aggregation_group` types render as an area band; different groups render as separate lines. Because the method is per-type, a single visualization can mix bar-shaped and line-shaped series. ```json title="Visualization metadata hook" }/token", "method": "GET", "headers": }", "UID": "}" }, "cache": }-}", "ttl": "3600" } }, "call": }/example/visualization/metadata", "headers": }" }, "params": }" } }, "resolved": } ``` > **Note: `use_static_ips` is deprecated on all hook types — prefer `secure_proxy` (route requests through the ERP Integration secure proxy by setting `integration_id` and `use_case_slug`). The `resolved.dataPath` field has also been renamed to `resolved.data_path`; the old name still works but is deprecated.** #### Examples The epilot **example integration** service is a reference backend that powers Dynamic Tariff and Consumption blocks against synthetic-but-realistic German energy data. It exposes the data-retrieval endpoints (`/example/price`, `/example/consumption`, `/example/cost`) and a `/example/visualization/metadata` endpoint, and accepts a `setup` query parameter that selects a predefined deployment scenario. Using the same `setup` across the metadata and data hooks keeps the advertised `type_options` aligned with the records the data hooks return. The endpoints also accept `from`, `to` and `interval` (and, for consumption/cost, an optional `multiplier` for B2B scenarios). The two scenarios below show the prosumer and load-cycle setups end-to-end. ##### Prosumer (feed-in / feed-out) A household with a rooftop PV system both imports from and exports to the grid. The `prosumer` setup advertises two series — `feed-in` (surplus exported to the grid, peaks midday) and `feed-out` (drawn from the grid, mostly at night) — as separate groups so they render as distinct series. ![Prosumer visualization example](/img/apps/portal-extensions/prosumer.png) ```json title="Visualization metadata response (setup=prosumer)" , "aggregation_group": "feed-in", "statistical_method": "sum", "unit": "kWh", "color": "green", "precision": 2 }, , "aggregation_group": "feed-out", "statistical_method": "sum", "unit": "kWh", "color": "blue", "precision": 2 } ], "intervals": ["PT15M", "PT1H", "P1D", "P1M"], "data_range": } ``` ```json title="Consumption hook (setup=prosumer)" , "auth": }/token", "method": "GET", "headers": }", "UID": "}" }, "cache": }-}", "ttl": "3600" } }, "call": }/example/consumption", "headers": }" }, "params": }", "to": "}", "interval": "}", "setup": "prosumer" } }, "resolved": } ``` The matching consumption response (hourly) returns a `feed-in` and a `feed-out` record per timestamp — surplus during the day, grid draw at night: ```json title="Consumption response (setup=prosumer, interval=PT1H)" , , , , , ] } ``` For a line-chart variant of the same data, the example integration also ships a `prosumer-line` setup (feed-in/feed-out advertised with `statistical_method: average`). ##### Load Cycle (min / average / max) An industrial / B2B site is billed on instantaneous power (kW) rather than energy. The `load-cycle` setup advertises three series — `min`, `average` and `max` — sharing a single `aggregation_group` but each carrying its own `statistical_method`. The portal renders this as a min–max area band with the average drawn as a line on top. ![Prosumer visualization example](/img/apps/portal-extensions/load-cycle.png) ```json title="Visualization metadata response (setup=load-cycle)" , "aggregation_group": "load", "statistical_method": "min", "unit": "kW", "color": "slate", "precision": 0 }, , "aggregation_group": "load", "statistical_method": "average", "unit": "kW", "color": "primary", "precision": 0 }, , "aggregation_group": "load", "statistical_method": "max", "unit": "kW", "color": "red", "precision": 0 } ], "intervals": ["PT15M", "PT1H", "P1D"], "data_range": } ``` ```json title="Consumption response (setup=load-cycle, interval=PT1H)" , , , , , ] } ``` The hook configuration is identical to the prosumer consumption hook above, only with `"setup": "load-cycle"` in `call.params`. ##### Other Example Setups The example integration ships further setups you can point the `setup` parameter at to exercise different chart shapes and discovery payloads: | `setup` | Series (`type_options`) | Rendered as | Notes | | --- | --- | --- | --- | | `default` | `default` (Consumption, kWh) | Bar | Single-tariff household, all intervals, last 2 years. | | `dual-tariff` | `ht` (High tariff), `nt` (Night tariff) | Stacked bar | HT 06:00–22:00, NT otherwise; both in the `consumption` group. | | `prosumer` | `feed-in`, `feed-out` | Bars (separate groups) | Net grid flows for a 5 kWp PV household. | | `prosumer-line` | `feed-in`, `feed-out` | Area / line | Prosumer data as `statistical_method: average`. | | `load-cycle` | `min`, `average`, `max` | Min–max band + average line | Industrial load in kW. | | `consumption-with-load` | `consumption` (kWh, bar), `average-load` (kW, line) | Mixed bar + line | Two `aggregation_group`s in one chart. | | `daily-only` | `default` | Bar | Only `P1D` + `P1M` (e.g. monthly meter readings). | | `partial-history` | `ht`, `nt` | Stacked bar | Last 6 months only (e.g. recently switched provider). | | `current-month` | `default` | Bar | Current month only (`PT1H` + `P1D`), e.g. start-of-contract demos. | ### Data Existence Check/Retrieval Sometimes it is desired to check against a third party system before allowing a user to register or self-assign business objects to their account. At the same time, it might be necessary to load business entities to epilot before allowing user to proceed in cases epilot does not have all data on non-portal users. #### Registration Hook Use the registration hook to validate identifiers before creating a portal user. If the registration is valid, you can pass back an epilot Contact UUID that is associated with the portal user. If no body is specified, all identifiers configured for the portal and provided by the user are passed grouped by the schema. ```json title="Registration hook" }" }, "result": "}" } } ``` #### Template Variables Registration hooks support the standard template variables plus the identifiers context: - **`Options.*`**: Access values from the app options configured during installation - **`Identifiers.*`**: Access properties provided by the user to identify the entity groupped by schema #### Self-Assignment Hook Use the self-assignment hook when portal users attach additional contracts to their account. The hook can also include localized explanations shown to the user. If no body is specified, all identifiers configured for the portal and provided by the user are passed grouped by the schema. ```json title="Self-assignment hook" }" }, "body": }", "portal_user_id": "}", "contract_number": "}" } } }, "assignment_mode": "contact_to_portal_user", "explanation": } ``` #### Template Variables Self-assignment hooks support the standard template variables plus the identifiers context: - **`Options.*`**: Access values from the app options configured during installation - **`Contact.*`**: Access properties from the current portal user's contact entity - **`PortalUser.*`**: Access properties from the current portal user - **`Identifiers.*`**: Access properties provided by the user to identify the entity groupped by schema - **`CallResponse.*`**: Access data from the call response (for example in `call.result`) ### Data Validation #### Meter Reading Plausibility Use the plausibility check hook to validate meter readings before they are submitted and return limits for validation feedback. ```json title="Meter reading plausibility check hook" }" }, "body": }", "register_number": "}", "timestamp": "}", "value": "}" } }, "resolved": }", "lower_limit": "}", "upper_limit": "}" } } ``` #### Template Variables Meter reading plausibility hooks support the standard template variables plus meter reading context: - **`Options.*`**: Access values from the app options configured during installation - **`Contact.*`**: Access properties from the current portal user's contact entity - **`Meter.*`**: Access properties of the meter - **`MeterCounter.*`**: Access properties of the meter register - **`Reading.*`**: Access properties of the submitted reading - **`CallResponse.*`**: Access data from the call response For questions about portal extensions, [contact our developer support team](https://developers.epilot.cloud/contact). --- # Permissions Learn how to configure permissions your App requires to function properly. When building an app for the epilot platform, you may need to request specific permissions to access certain features or data. This guide will help you understand how to configure these permissions effectively. ## What Are Permissions? Permissions in epilot control access to various features and data within the platform. When you create an app, you can specify which permissions your app requires to function correctly. This ensures that users have the necessary access rights to use your app without encountering permission-related issues. ## How to Configure Permissions For Your App To configure permissions for your app, you need to create a role in your own organization and specify which grants (permissions) this role should have. This role can then be assigned to users who will be using your app. ### Step 1: Create a Role 1. Go to the **Access Management** section of your epilot organization (under settings). 2. Navigate to the **Roles** tab. 3. Click on **New Role**. 4. Enter a name for the role (e.g., "App User Role"). 5. Click **Save** to create the role. > **Info: A dedicated **App Role** type is coming soon. For now, please select the **User Role** for the role type.** ### Step 2: Configure Permissions 1. After creating the role, you will see a list of available permissions (grants). 2. Go to your App configuration in the epilot platform. 3. In the **Permissions** section, select the role you created in Step 1. and save the changes. ## How To Use Permissions in Your App Once your App is installed, a role you specified is automatically created in the customer's organization. This role will have the permissions you configured in Step 2. Now every component has different options to access those permissions. For example the Custom Action component can access the permissions through the `x-epilot-token` header in the http request. This token contains the necessary information to authenticate and authorize the action being performed. Check the specific component documentation for details on how to access permissions in your app. ## Supported Permissions by Component Type | Component Type | Permissions Supported | |---------------------------|-----------------------| | Custom Journey Block | No | | Portal Extension | No | | Custom Action | Yes | | API Proxy | No | --- Learn how to run the App in development now to get changes reflected live. # What is Development Mode? > **Tip: Development mode lets you see changes in real-time without creating new versions. Enable it during active development to skip the upload-install-test cycle.** Development mode is a feature that allows you to run your app in a local development environment. This mode enables you to see changes in real-time without having to update the app every time you make a change. It is particularly useful for testing and debugging your app during the development process. ## How to Run in Development Mode Head over to the components section and you will find a button to run the app in development mode. It enables development mode for the ***latest private version*** of your App. Once enabled, this particular version is pinned and cannot be changed. You can make changes and immediately see the results without reinstalling your App each time. With development mode enabled, you also have the ability to override certain component configuration values. ![Development Mode Overview](/img/apps/dev-mode-overview.png) ![Development Mode Versions](/img/apps/development-mode-enable.png) ## Custom Journey Block Overrides With development mode enabled, you can override the component URL of your Custom Journey Block. This allows you to point the component to a local development server or any other URL where your component is hosted. This way, you can test changes in real-time without having to publish a new version of your app. ![Development Mode Overview](/img/apps/override-cjb.png) Now once you go into a journey to connect this custom block, you will see the component url is overridden with the one you provided in the development mode. This allows you to test your changes in the context of a journey without having to publish a new version of your app. --- # Deploying & Updating Functions ## Adding a function ```bash # workflow function (code behind a flow action component) npx @epilot/cli app add-function reserve-slot --type workflow npx @epilot/cli app add-component reserve-slot-action --type CUSTOM_FLOW_ACTION_FUNCTION # scheduled function (--schedule implies --type scheduled) npx @epilot/cli app add-function sync-things --schedule "rate(30 minutes)" ``` This scaffolds `functions//` as its own workspace package (TypeScript, `tsc` build to `dist/handler.js`) and registers the function in `manifest.json`. The `handler` path in the manifest points at the **built** file: ```json ``` ## Deploying ```bash npm run build # compile all functions npx @epilot/cli app validate # same checks the platform runs (schedule rules, code contract) npx @epilot/cli app deploy ``` `deploy` reads each function's built handler, inlines the code into your app version, and uploads any config-UI assets. The manifest is the single source of truth: the deployed set of functions **always exactly matches** the manifest — removing a function from the manifest removes it (and its schedules) from the version. ## How updates reach installations Functions are **versioned with your app**. An installation runs the function code of its *installed version* — deploying new code does not silently change what runs in customer organizations: 1. **Unpublished version** (still in development): `deploy` updates the version in place. Installations move to the new code when they update to the version. 2. **Published (public) versions are immutable**: `deploy` automatically creates a new version. Installing orgs receive it through the regular update flow ("Update to latest" / automatic updates), which also reconciles schedules — new scheduled functions start, removed ones stop, changed cron expressions take effect. 3. **Your own dev org**: with [development mode](/docs/apps/development-mode), your test installation follows the development version so you can iterate without version-bumping. There is no separate "function update" mechanism to think about: ship a version, installations that move to it run its functions. Run bookkeeping (last run, failure counts) survives version updates as long as the function keeps its name. ## Renaming and removing - **Renaming** a scheduled function is a remove-plus-add: the old schedule (and its run bookkeeping) is dropped, a fresh one is created. Renaming a **workflow** function requires updating the referencing component's `function_name` in the same deploy — the reference is validated, so a dangling name is rejected before it can break anything. - **Removing** a scheduled function stops its schedules in every installation on their next version update; removing a workflow function requires removing (or repointing) its referencing component too. Flows that used the removed action show it as unavailable — legitimate, just changelog it. ## Publishing review Functions are part of the app review when you [publish](/docs/apps/publishing/verification-process): reviewers see the code, the declared schedules and permissions. Dense schedules, unbounded loops or undeclared data access are the typical rejection reasons — the [limits](/docs/apps/functions/writing-functions#limitations) exist so a published app can never degrade the platform for anyone else. --- # App Functions Functions are **server-side JavaScript that runs inside epilot** — no infrastructure of your own, no exposed endpoints, no credentials in the browser. You write a handler, declare it in your app's manifest, deploy with the CLI, and epilot executes it in a secured sandbox on your behalf. Functions are the unit for all custom server-side logic in an app. Where they run is decided by their **type**: | Type | Triggered by | Typical use | |---|---|---| | `workflow` | A `CUSTOM_FLOW_ACTION` component references it (``); org admins add that action in the flow builder and it runs with the triggering entity as input | Enrich an entity, call your API through the [API Proxy](/docs/apps/components/api-proxy), validate data, reserve something in an external system | | `scheduled` | A **cron schedule**, automatically, once per installation | Poll a system that has no webhooks, sync a catalog nightly, refresh cached data | ```json title="manifest.json" , ] } ``` ## Functions vs. components Components are the **surfaces** of your app — journey blocks, custom pages, portal blocks, API proxies. Functions are its **behavior**. They complement each other: - A `workflow` function is wired into the flow builder through a `CUSTOM_FLOW_ACTION` component that references it — the component carries the org-facing name and config UI; the function carries the code. (`external_integration` components remain for [webhook calls](/docs/apps/components/custom-action) to *your* servers.) - A `scheduled` function runs without any user interaction at all. - Functions can call external APIs through your app's **API Proxy** component, so external credentials stay on the installation and never appear in function code. ## Code-first by design There is no code editor in the epilot UI. Functions live in your app repository, are validated at deploy time, versioned with your app, and reviewed when you publish. This is deliberate: scheduled and flow-triggered code must be reproducible per version and per installation — a UI-edited snippet can be neither. ```bash npx @epilot/cli app add-function reserve-slot --type workflow --label "Reserve slot" npm run build npx @epilot/cli app deploy ``` ## What installing organizations see - **Workflow functions** show up in the flow builder's action picker through their referencing component, under the component's name. - The installed app's details page shows **scheduled** functions in a compact summary on the **Configuration tab** — label plus a plain-language cadence ("every 30 minutes") — transparency about what the app runs on the org's behalf in the background. Workflow functions need no extra listing: they appear as their flow-action component cards right above. - Every run is recorded in the app's **Insights**, so you (the developer) can monitor failures per version and component. Continue with [Writing functions](/docs/apps/functions/writing-functions) for the runtime contract. --- # Scheduled Functions A function with `type: "scheduled"` runs automatically on a cron schedule — **once per installation**. If ten organizations install your app, your function runs ten times per tick, each run isolated to one organization's data, options and permissions. ```json title="manifest.json" ] } ``` ## Schedule expressions Two formats, both validated at deploy time (and locally by `epilot app validate`): | Format | Example | Meaning | |---|---|---| | Rate | `rate(30 minutes)`, `rate(1 hour)`, `rate(2 days)` | Fixed interval | | 5-field cron | `0 3 * * *` | [crontab.guru](https://crontab.guru/)-compatible; evaluated in `schedule_timezone` (default `Europe/Berlin`) | **Rules:** - Minimum interval: **15 minutes** — denser expressions are rejected, including list tricks like `0,5 * * * *`. - At most **5 scheduled functions** per app. - Don't restrict day-of-month *and* day-of-week in the same expression — set one of them to `*`. ## Execution semantics - **Jitter**: runs execute within roughly **15 minutes after** the scheduled time, not at the exact second. Design for "around 3 AM", not "at 03:00:00". - **Hard 60-second budget** per run. - **No overlap**: if the previous run is still going, the tick is skipped. - **At-least-once**: a failed delivery is retried; occasionally a tick may run twice — make your logic idempotent. - **Lifecycle**: schedules are created when an org installs your app, updated when the installation moves to a new version, and deleted on uninstall. A disabled installation (missing required options) is skipped. ## Do bounded work per tick 60 seconds is a budget, not a target. The reliable pattern is incremental processing: handle a bounded batch, let the next tick pick up the rest. ```ts const MAX_ITEMS_PER_RUN = 150; const TIME_BUDGET_MS = 45_000; async function handler(input, context) return ; } ``` Two more habits that keep scheduled syncs well-behaved: - **Write only on change.** Diff before you PATCH an entity — otherwise every tick touches every entity and fires the org's entity-based automations for nothing. - **Let single items fail individually.** Log per-item errors and continue; only return `` when the whole run is broken (e.g. the target system is down), so failures in your Insights mean something. ## Observability Every run is recorded in your app's **Insights** (source `APP_FUNCTION`) — successes, skips and failures with their messages, per version and per installing organization. Installing orgs see the schedule as a plain-language summary ("every 30 minutes") on the installed app's **Configuration tab**. --- # Workflow Functions A function with `type: "workflow"` provides the **code behind a flow action**. The pairing works like this: a `CUSTOM_FLOW_ACTION` component is the org-facing contract — its name appears in the flow builder's action picker, it carries the optional per-flow config UI — and its configuration **references the function that runs**: ```json title="manifest.json" ], "components": [ , "description": , "options": [ ], "configuration": } ] } ``` Why the split? The component reuses everything components already have — display name and description in the picker, the config surface — while the function stays a pure unit of code and reads the installation's [app options](/docs/apps/app-options) from `input.app_options`. Deploy-time validation checks the reference: the named function must exist in the same version with `type: "workflow"`. When the org admin adds the action to a flow and the flow reaches that step, epilot runs your handler with the triggering entity. ## The run ```ts async function handler(input, context) = input.app_options; if (!entity?.my_required_field) ; } // ... call your API via the proxy, write results back via the Entity API ... return ; } ``` Workflow runs block a flow step, so keep them fast — do one thing per action. Long-running work belongs in a [scheduled function](/docs/apps/functions/scheduled-functions) or behind `wait_for_callback`. ## Installation options Options declared on the **component** are filled in by the org admin at install time and reach your handler as `input.app_options` (secret options stay encrypted; declare the ones your function needs in the function's `secrets` list — or better, use an [API Proxy](/docs/apps/components/api-proxy)). ## Per-flow configuration UI (optional) If your action needs configuration when it's added to a flow (mappings, mode switches), ship a config UI on the **component**, exactly as flow actions always have — `surfaces.flow_action_config` plus `assets.zip`. Values saved by your config UI arrive in the handler as `input.action_config.custom_action_config`. ## Waiting for a callback Set `wait_for_callback: true` in the **component's** configuration when the action starts something asynchronous (e.g. a human approval in your system) and the flow should pause until you confirm. The flow execution pauses at your action and resumes when your system calls the automation resume endpoint with the execution's resume token. ## Failure behavior - `` fails the flow step; the message is shown to the flow's operator and recorded in your app's Insights. - `` marks the step skipped — use it for "nothing to do here" instead of failing. --- # Writing Functions Every function is a single JavaScript file that declares a top-level handler: ```ts title="functions/my-function/src/handler.ts" async function handler(input, context) ; } ``` That's the whole contract — **no `export`**, no framework, no wrapper. The file you deploy is the built output (`dist/handler.js`); the CLI inlines it into your app version on `epilot app deploy`. Scaffolding via `epilot app add-function` gives you a TypeScript setup that compiles to exactly this shape. ## The `input` object What your handler receives depends on the trigger: ```ts , app_options: , // Workflow runs entity: , // the entity the flow ran on action_config: , // the action's configuration from the flow builder, // including values from your custom config UI // Scheduled runs org_id: "739224", // the installation this run belongs to app_id: "your-app-id" } ``` ## The `context` object ```ts context.epilot // the full @epilot/sdk, pre-authorized with your app token context.fetch // standard fetch() for everything else ``` The **epilot SDK is bundled into the sandbox** — you don't import or install anything, and `epilot.authorize()` is already called with `input.app_options.token`. Every epilot API is available as `context.epilot..(...)`, fully typed against the platform's OpenAPI specs. `console.log()` output is captured per run; for scheduled functions the last ~20 lines are attached to the run's event in your app's **Insights**, so a summary log at the end of your handler doubles as run diagnostics. ## Return values | Return | Effect | |---|---| | any object, e.g. `` | Run succeeded | | `` | Run intentionally skipped — a workflow action is marked skipped, a scheduled run counts as skipped | | `` | Run failed — a workflow action fails the flow step, a scheduled run is recorded as an error | | throwing an exception | Same as `error_reason`, with a generic message shown to the user | ## Calling epilot APIs Use the bundled SDK — no auth wiring, no base URLs, typed operations: ```ts async function handler(input, context) = context; // Search entities const = await epilot.entity.searchEntities(null, ); // Update an entity for (const entity of data.results ?? []) , ); } return ; } ``` Everything the SDK offers is there: `epilot.entity`, `epilot.pricing`, `epilot.workflow`, `epilot.message`, … — see the [SDK reference](https://github.com/epilot-dev/sdk-js) for the full list of APIs and operations. **What the SDK runs as:** the SDK is pre-authorized with `input.app_options.token` — an **app token**, short-lived, minted per run, scoped to the installing organization and to exactly the [permissions](/docs/apps/configure-permissions) your manifest declares. If your manifest declares `entity:view` on `opportunity`, that is all your function can do — in that organization's data, never anyone else's. > **Note: Non-production stages** The bundled SDK targets epilot's production APIs. When testing an app against a non-production epilot environment, call the APIs with `context.fetch` instead, deriving the base URL from `input.app_options.stage` (e.g. `https://entity.$.sls.epilot.io`) and sending `Authorization: Bearer $` yourself. In customer organizations (production) the SDK is always the right tool. ## Calling external APIs Route external calls through your app's [API Proxy](/docs/apps/components/api-proxy) component. Credentials (API keys, OAuth secrets) are configured per installation as secret [app options](/docs/apps/app-options) and injected server-side by the proxy — your function never sees them. The nicest way is the proxy wrapper from **`@epilot/app-sdk`** — the same one your frontend components use. Since function code ships as a single file, bundle your handler (e.g. with esbuild) so the import is inlined: ```ts async function handler(input, context) ); const order = await client.proxy("my-api", "/orders/4711"); const created = await client.proxy("my-api", "/reservations", , }); // ... } ``` If your build is plain `tsc` (no bundler), call the proxy URL directly with `context.fetch` — same request, just hand-rolled: ```ts const = input.app_options; const base = `https://app$` : ""}.sls.epilot.io`; const res = await context.fetch( `$/v1/public/app/$/proxy/my-api/orders/4711`, ` } } ); ``` Both forms hit `POST/GET …/v1/public/app//proxy//` with the app token as Bearer — the proxy injects the real credentials server-side. (The wrapper also takes `baseUrl` if you target a non-production stage.) If a function genuinely needs a raw secret (rare — prefer the proxy), just read it from `input.app_options` — functions receive **all** [app option](/docs/apps/app-options) values, including sensitive and secret ones, since they always run server-side. (The per-function `secrets` allowlist that older manifests declared is deprecated and ignored.) ## Limitations | Limit | Value | |---|---| | Code size | 300 KB hard limit per function (warning above 100 KB) — ship a single bundled file, no `node_modules` at runtime | | Execution time | Workflow runs: seconds (they block a flow step). Scheduled runs: **60 seconds hard** | | Memory | 10 MB sandbox default | | Language | JavaScript/TypeScript syntax, script context — no `import`/`export`, no `require`. The epilot SDK is built in as `context.epilot`; other libraries must be bundled into your handler file | | Forbidden | `eval()`, the `Function()` constructor — rejected at deploy time | | Environment | No filesystem, no environment variables, no Node.js APIs — network via `context.epilot` and `context.fetch` only | | Isolation | One run = one installation. The token, options and data access are always scoped to a single organization | Deploy-time validation enforces the contract: syntax is parsed, a `handler` declaration is required, size and security rules are checked — `epilot app validate` runs the same checks locally before you deploy. --- # Getting Started with App Development Your safe space to experiment, build, and perfect your epilot App

Safe Experimentation

Explore the full capabilities of the epilot platform without risking production environments. Your sandbox is your personal laboratory for innovation.

Rapid Development

Build, test, and refine your apps in an environment that mimics production but forgives mistakes. Iterate quickly without fear of breaking changes.

## Why You Need a Sandbox The standard epilot portal is designed for production environments where precision is paramount. Even minor configuration errors could potentially: - Disrupt running systems - Cause data inconsistencies - Affect end-customer experiences - Undermine trust in your solutions This is precisely why epilot provides dedicated sandbox accounts—isolated environments specifically designed for developers to experiment, learn, and innovate without these risks. ## Your App Development Journey
1

Request a Sandbox

Submit your request through our Developer Portal

2

Access Your Environment

After approval, sign in with your provided credentials and explore your new development space

3

Create Your First App

Navigate to the app creation page and start building

4

Test & Refine

Thoroughly test your app in various scenarios and refine until perfect

5

Submit for Review

When you're confident in your app, submit it for review to make it publicly available

## Benefits of Sandbox Development

Complete Isolation

Experiment freely without affecting other systems or users in the epilot ecosystem

Comprehensive Testing

Test your app thoroughly in a realistic environment that matches production

Full Configuration

Access all the necessary tools and features to build sophisticated apps

Risk-Free Debugging

Find and fix issues without concern for production impacts

## From Private to Public Your initial app development takes place in a protected environment: 1. **Private Stage**: Your app is only available for installation within your sandbox account 2. **Review Process**: When ready, submit your app for a formal review 3. **Public Launch**: After approval, your app becomes available to the entire epilot community To learn more about transitioning from development to publication, visit our detailed guide in the [Publishing](/docs/apps/publishing/verification-process) section.

Ready to start building?

Request Your Sandbox Today