# Delete Agent Source: https://docs.emergence.ai/api-reference/agents/delete-agent /openapi/em-runtime-assets-v2.yaml delete /assets/agents/{resource_uri} Delete an agent permanently. # Get Agent Source: https://docs.emergence.ai/api-reference/agents/get-agent /openapi/em-runtime-assets-v2.yaml get /assets/agents/{resource_uri} Get a registered agent by resource URI. # List Agents Source: https://docs.emergence.ai/api-reference/agents/list-agents /openapi/em-runtime-assets-v2.yaml get /assets/agents List registered agents in project. # Register Agent Source: https://docs.emergence.ai/api-reference/agents/register-agent /openapi/em-runtime-assets-v2.yaml post /assets/agents Register a new agent from an A2A agent card. # Update Agent Source: https://docs.emergence.ai/api-reference/agents/update-agent /openapi/em-runtime-assets-v2.yaml put /assets/agents/{resource_uri} Update an agent's card and metadata (optimistic concurrency via ETag). # Validate Agent Card Source: https://docs.emergence.ai/api-reference/agents/validate-agent-card /openapi/em-runtime-assets-v2.yaml post /assets/agents:validate Dry-run validation of an agent card (no persistence). # Delete Artifact Source: https://docs.emergence.ai/api-reference/artifacts/delete-artifact /openapi/em-runtime-assets-v2.yaml delete /assets/artifacts/{resource_uri} Delete artifact # Download artifact Source: https://docs.emergence.ai/api-reference/artifacts/download-artifact /openapi/em-runtime-assets-v2.yaml get /assets/artifacts/{resource_uri}/download Download an artifact file from object storage # Get Artifact Source: https://docs.emergence.ai/api-reference/artifacts/get-artifact /openapi/em-runtime-assets-v2.yaml get /assets/artifacts/{resource_uri} Get artifact details # List Artifacts Source: https://docs.emergence.ai/api-reference/artifacts/list-artifacts /openapi/em-runtime-assets-v2.yaml get /assets/artifacts List artifacts in project # Preview artifact content Source: https://docs.emergence.ai/api-reference/artifacts/preview-artifact-content /openapi/em-runtime-assets-v2.yaml get /assets/artifacts/{resource_uri}/preview Return a structured preview of an artifact's content. Supports CSV, TSV, plain text, SQL, markdown, HTML, XML, YAML, JSON, and JSONL. # Update Artifact Source: https://docs.emergence.ai/api-reference/artifacts/update-artifact /openapi/em-runtime-assets-v2.yaml put /assets/artifacts/{resource_uri} Update artifact metadata # Upload artifact Source: https://docs.emergence.ai/api-reference/artifacts/upload-artifact /openapi/em-runtime-assets-v2.yaml post /assets/artifacts Upload a file to object storage and create artifact record # Export audit events to CSV Source: https://docs.emergence.ai/api-reference/audit/export-audit-events-to-csv /openapi/em-runtime-governance-v2.yaml get /governance/audit/export Export audit events as a CSV file with pagination. Results are always scoped to the user's organization. Use page/page_size to paginate large exports (max 50k rows per page). # Get audit event by ID Source: https://docs.emergence.ai/api-reference/audit/get-audit-event-by-id /openapi/em-runtime-governance-v2.yaml get /governance/audit/{event_id} Get a specific audit event by its ID. Returns 404 if the event doesn't exist or belongs to a different organization. # Get audit settings Source: https://docs.emergence.ai/api-reference/audit/get-audit-settings /openapi/em-runtime-governance-v2.yaml get /governance/audit/settings Get audit settings (retention period) for the current organization. Returns default settings if none have been configured. # List audit events Source: https://docs.emergence.ai/api-reference/audit/list-audit-events /openapi/em-runtime-governance-v2.yaml get /governance/audit List audit events for the current organization with optional filters. Results are always scoped to the user's organization (no cross-org access). Requires organization admin permissions. # Update audit settings Source: https://docs.emergence.ai/api-reference/audit/update-audit-settings /openapi/em-runtime-governance-v2.yaml patch /governance/audit/settings Update audit settings for the current organization. If retention is reduced, events outside the new retention window are immediately deleted. Requires organization admin permissions. # API Authentication Source: https://docs.emergence.ai/api-reference/authentication How to authenticate with CRAFT APIs using JWT tokens, client credentials, and the auto-generated SDKs. # API Authentication All CRAFT API endpoints require authentication via JWT tokens issued by Keycloak. This page covers how to obtain tokens, authenticate requests, and handle token refresh. ## Authentication Methods For interactive applications, use the **OIDC Authorization Code flow with PKCE**: 1. Redirect the user to the Keycloak authorization endpoint 2. Exchange the authorization code for tokens 3. Include the access token in API requests ``` GET https:///realms//protocol/openid-connect/auth ?response_type=code &client_id= &redirect_uri= &scope=openid email profile &code_challenge= &code_challenge_method=S256 ``` After the user authenticates, exchange the code: ```bash theme={null} curl -X POST \ "https:///realms//protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=" \ -d "code=" \ -d "redirect_uri=" \ -d "code_verifier=" ``` For service accounts, CI/CD pipelines, and automation, use the **client credentials grant**: ```bash theme={null} curl -X POST \ "https:///realms//protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=" \ -d "client_secret=" ``` Response: ```json theme={null} { "access_token": "", "expires_in": 300, "token_type": "Bearer", "scope": "openid email profile" } ``` Client credentials tokens do not include a refresh token. Obtain a new access token before expiration. For local development, use the `make get-token` shortcut in the em-runtime repository: ```bash theme={null} # Get a token for the org admin user make get-token # Get a platform admin token (Keycloak master realm) make get-platform-token ``` Or use `curl` directly: ```bash theme={null} TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms//protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=admin" \ | jq -r '.access_token') ``` The password grant is intended for development only. Never use it in production -- use OIDC with PKCE or client credentials instead. ## Using the Token Include the access token in the `Authorization` header of every API request: ```bash theme={null} curl -X GET "https:///api/governance/organizations" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " ``` ### Required Headers | Header | Required | Description | | --------------- | ---------------- | -------------------------- | | `Authorization` | Always | `Bearer ` | | `X-Project-ID` | Most endpoints | UUID of the target project | | `Content-Type` | Write operations | `application/json` | The `X-Project-ID` header determines which project the request is scoped to. A single JWT token can access multiple projects within the same organization. The project ID is never embedded in the JWT. ## Token Refresh Access tokens have a short lifetime (typically 5 minutes). Use the refresh token to obtain new access tokens without re-authentication: ```bash theme={null} curl -X POST \ "https:///realms//protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=" \ -d "refresh_token=" ``` ### Refresh Strategy Track the `exp` claim in the JWT payload. Plan to refresh when 75% of the token lifetime has elapsed. Refresh the token before it expires to avoid 401 errors during in-flight requests. If the refresh token is expired or revoked, redirect the user to the login flow. For service accounts, obtain a new token via client credentials. ## SDK Authentication The auto-generated SDKs follow the [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) and `openapi-generator` patterns — an `AuthenticatedClient` with a `token` constructor arg + per-endpoint module functions, **not** an object/fluent client. See the [Python SDK](/sdks/python) and [TypeScript SDK](/sdks/typescript) overviews for the full client model. ```python theme={null} from em_runtime_governance_sdk import AuthenticatedClient from em_runtime_governance_sdk.api.organizations import list_organizations client = AuthenticatedClient( base_url="https:///api/governance", token=access_token, ) orgs = await list_organizations.asyncio(client=client) # Token refresh: pop the expired client and rebuild with a fresh token. # The SDK has no built-in onTokenExpired callback; wrap your calls in a # helper that catches 401, refreshes, and retries with a new client. ``` ```typescript theme={null} import { Client } from '@emergence-ai/em-runtime-governance-sdk'; import { listOrganizations } from '@emergence-ai/em-runtime-governance-sdk/api'; const client = new Client({ BASE: 'https:///api/governance', TOKEN: accessToken, }); const orgs = await listOrganizations({ client }); ``` ## Token Claims The JWT access token contains these claims used by the platform: ```json theme={null} { "sub": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "iss": "https://keycloak.example.com/realms/org-123", "aud": "emergence-platform", "exp": 1712150400, "iat": 1712150100, "groups": ["admins", "developers"], "email": "user@example.com", "preferred_username": "jdoe" } ``` | Claim | Platform Usage | | -------- | ---------------------------------------------------- | | `sub` | Unique user identifier for audit logs | | `iss` | Organization ID derived from realm in the issuer URL | | `groups` | Mapped to OpenFGA roles for permission checks | | `exp` | Token expiration validation | ## Error Responses Authentication failures return structured error responses: | Status | Code | Description | | ------ | --------------------- | ----------------------------------------------------------- | | `401` | `UNAUTHENTICATED` | No token provided or token signature invalid | | `401` | `TOKEN_EXPIRED` | Access token has expired; refresh and retry | | `403` | `FORBIDDEN` | Token is valid but user lacks permission for this operation | | `429` | `RATE_LIMIT_EXCEEDED` | Too many requests; check `Retry-After` header | ## Next Steps Review the full API structure, endpoints, and rate limits. Understand all error response formats and troubleshooting steps. Configure SSO for enterprise identity providers. Set up roles and permissions for API access control. # Get bootstrap status Source: https://docs.emergence.ai/api-reference/bootstrap/get-bootstrap-status /openapi/em-runtime-governance-v2.yaml get /governance/bootstrap/status Returns information about the bootstrap configuration and default organization. Bootstrap happens automatically during application startup. The default organization id is the Keycloak-assigned UUID (looked up by configured display name). Requires platform developer role. # Add context items Source: https://docs.emergence.ai/api-reference/chat-sessions/add-context-items /openapi/em-talk2data-v2.yaml post /talk2data/chat/sessions/{session_id}/context Add or update datasources, artifacts, and/or attachments on a session. Upserts by (type, resource_uri). Returns the full updated context. 403 if not owned, 404 if not found. # Delete feedback Source: https://docs.emergence.ai/api-reference/chat-sessions/delete-feedback /openapi/em-talk2data-v2.yaml delete /talk2data/chat/sessions/{session_id}/turns/{turn_id}/feedback Delete the current user's feedback for the turn. 403 if not owned, 404 if session/turn/feedback not found. # Delete session Source: https://docs.emergence.ai/api-reference/chat-sessions/delete-session /openapi/em-talk2data-v2.yaml delete /talk2data/chat/sessions/{session_id} Delete a session and cascade-delete messages, artifacts, and feedback. 403 if not owned, 404 if not found. # Get session Source: https://docs.emergence.ai/api-reference/chat-sessions/get-session /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id} Get session metadata with turn_count, message_count, artifact_count. Returns 403 if session does not belong to user/project, 404 if not found. # Get session context Source: https://docs.emergence.ai/api-reference/chat-sessions/get-session-context /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id}/context Get all datasources, artifacts, and attachments pinned to this session. 403 if not owned, 404 if not found. # Get turn by ID Source: https://docs.emergence.ai/api-reference/chat-sessions/get-turn-by-id /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id}/turns/{turn_id} Get a single turn with all its messages and ``task_updates`` (same shape as list turns). 403 if session not owned, 404 if session or turn not found. # List session artifacts Source: https://docs.emergence.ai/api-reference/chat-sessions/list-session-artifacts /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id}/artifacts Paginated artifacts for a session. Optional artifact_type filter (parquet, chart). 403 if session not owned, 404 if not found. # List session turns Source: https://docs.emergence.ai/api-reference/chat-sessions/list-session-turns /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id}/turns Paginated turns (user + assistant messages grouped by turn_id). Each turn includes ``task_updates`` from ``talk2data.task_updates`` (A2A status stream), peer to ``messages``. Turn order by first message time: `order=asc` (oldest first, default) or `order=desc` (newest first). 403 if session not owned, 404 if not found. # List sessions Source: https://docs.emergence.ai/api-reference/chat-sessions/list-sessions /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions List sessions for the authenticated user and project. Paginated via page and limit. # List turn artifacts Source: https://docs.emergence.ai/api-reference/chat-sessions/list-turn-artifacts /openapi/em-talk2data-v2.yaml get /talk2data/chat/sessions/{session_id}/turns/{turn_id}/artifacts Paginated artifacts for a turn. Optional artifact_type filter (parquet, chart). 403 if session not owned, 404 if session or turn not found. # Remove context items Source: https://docs.emergence.ai/api-reference/chat-sessions/remove-context-items /openapi/em-talk2data-v2.yaml delete /talk2data/chat/sessions/{session_id}/context Remove one or more context items by (item_type, resource_uri). Returns the full updated context. 403 if not owned, 404 if not found. # Rename session Source: https://docs.emergence.ai/api-reference/chat-sessions/rename-session /openapi/em-talk2data-v2.yaml patch /talk2data/chat/sessions/{session_id} Update session title. 403 if session not owned, 404 if not found, 422 if title empty/blank or exceeds 255 chars. # Submit or update feedback Source: https://docs.emergence.ai/api-reference/chat-sessions/submit-or-update-feedback /openapi/em-talk2data-v2.yaml post /talk2data/chat/sessions/{session_id}/turns/{turn_id}/feedback Upsert feedback for a turn. 201 created, 200 updated. 403 if session not owned, 404 if session/turn not found, 422 if rating invalid. # Post Chat Cancel Source: https://docs.emergence.ai/api-reference/chat/post-chat-cancel /openapi/em-talk2data-v2.yaml post /talk2data/chat/messages/{task_id}/cancel Cancel an in-progress task. Sends A2A tasks/cancel to the orchestrator. Only allowed if the task's session belongs to the same user and project. # Post Chat Messages Source: https://docs.emergence.ai/api-reference/chat/post-chat-messages /openapi/em-talk2data-v2.yaml post /talk2data/chat/messages Accept a message and kick off background A2A processing. # Stream Chat Messages Source: https://docs.emergence.ai/api-reference/chat/stream-chat-messages /openapi/em-talk2data-v2.yaml get /talk2data/chat/messages/sessions/{session_id}/turns/{turn_id}/stream Stream persisted SSE events for a turn from durable storage. # Add Memory To Pack Source: https://docs.emergence.ai/api-reference/context-pack/add-memory-to-pack /openapi/em-memory-service-v2.yaml post /context-packs/{context_pack_name}/memories # Archive Pack Source: https://docs.emergence.ai/api-reference/context-pack/archive-pack /openapi/em-memory-service-v2.yaml post /context-packs/{context_pack_name}/archive # Create Pack Source: https://docs.emergence.ai/api-reference/context-pack/create-pack /openapi/em-memory-service-v2.yaml post /context-packs # Deprecate Pack Source: https://docs.emergence.ai/api-reference/context-pack/deprecate-pack /openapi/em-memory-service-v2.yaml post /context-packs/{context_pack_name}/deprecate # Get Pack By Name Source: https://docs.emergence.ai/api-reference/context-pack/get-pack-by-name /openapi/em-memory-service-v2.yaml get /context-packs/{context_pack_name} # List Pack Memories Source: https://docs.emergence.ai/api-reference/context-pack/list-pack-memories /openapi/em-memory-service-v2.yaml get /context-packs/{context_pack_name}/memories # List Packs Source: https://docs.emergence.ai/api-reference/context-pack/list-packs /openapi/em-memory-service-v2.yaml get /context-packs # Permanent Delete Pack Source: https://docs.emergence.ai/api-reference/context-pack/permanent-delete-pack /openapi/em-memory-service-v2.yaml post /context-packs/{context_pack_name}/permanent-delete # Reactivate Pack Source: https://docs.emergence.ai/api-reference/context-pack/reactivate-pack /openapi/em-memory-service-v2.yaml post /context-packs/{context_pack_name}/reactivate # Soft Delete Pack Source: https://docs.emergence.ai/api-reference/context-pack/soft-delete-pack /openapi/em-memory-service-v2.yaml delete /context-packs/{context_pack_name} # Update Pack Source: https://docs.emergence.ai/api-reference/context-pack/update-pack /openapi/em-memory-service-v2.yaml patch /context-packs/{context_pack_name} # Add a memory to a context pack Source: https://docs.emergence.ai/api-reference/context-packs/add-a-memory-to-a-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/memories Add a new memory to the context pack. # Archive a context pack Source: https://docs.emergence.ai/api-reference/context-packs/archive-a-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/archive Archive a context pack. # Create a new context pack Source: https://docs.emergence.ai/api-reference/context-packs/create-a-new-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs Create a new context pack in the project. # Deprecate a context pack Source: https://docs.emergence.ai/api-reference/context-packs/deprecate-a-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/deprecate Deprecate a context pack. # Get a context pack by name Source: https://docs.emergence.ai/api-reference/context-packs/get-a-context-pack-by-name /openapi/em-runtime-utils-v2.yaml get /utils/context-packs/{name} Get a context pack by name within the project. # Get a memory by name within a context pack Source: https://docs.emergence.ai/api-reference/context-packs/get-a-memory-by-name-within-a-context-pack /openapi/em-runtime-utils-v2.yaml get /utils/context-packs/{name}/memories/{memory_name} Get a memory by name within a context pack. # List context packs Source: https://docs.emergence.ai/api-reference/context-packs/list-context-packs /openapi/em-runtime-utils-v2.yaml get /utils/context-packs List context packs in the project with optional filters. # List memories in a context pack Source: https://docs.emergence.ai/api-reference/context-packs/list-memories-in-a-context-pack /openapi/em-runtime-utils-v2.yaml get /utils/context-packs/{name}/memories List memories in the context pack. # Permanently delete a context pack Source: https://docs.emergence.ai/api-reference/context-packs/permanently-delete-a-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/permanent-delete Permanently delete a context pack and all its memories. # Reactivate a context pack Source: https://docs.emergence.ai/api-reference/context-packs/reactivate-a-context-pack /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/reactivate Reactivate an archived or deprecated context pack. # Soft delete a context pack Source: https://docs.emergence.ai/api-reference/context-packs/soft-delete-a-context-pack /openapi/em-runtime-utils-v2.yaml delete /utils/context-packs/{name} Soft delete a context pack (can be recovered). # Update a context pack Source: https://docs.emergence.ai/api-reference/context-packs/update-a-context-pack /openapi/em-runtime-utils-v2.yaml patch /utils/context-packs/{name} Update a context pack's description, metadata, or config. # Batch-fetch latest column profiles by table and/or column FQN Source: https://docs.emergence.ai/api-reference/data-assets/batch-fetch-latest-column-profiles-by-table-andor-column-fqn /openapi/em-data-readiness-v2.yaml post /api/data-readiness/data-assets/profiles/columns/batch Return the latest column profile for every column matched by table_fqns and/or column_fqns. At least one filter is required (validated in the request model). Returns an empty list when nothing is profiled — does NOT 404. # Batch-fetch latest table profiles by table FQN Source: https://docs.emergence.ai/api-reference/data-assets/batch-fetch-latest-table-profiles-by-table-fqn /openapi/em-data-readiness-v2.yaml post /api/data-readiness/data-assets/profiles/tables/batch Return the latest table profile for each given table FQN. Returns an empty list when nothing is profiled — does NOT 404. # Clear DQ and/or metadata scores for a table Source: https://docs.emergence.ai/api-reference/data-assets/clear-dq-andor-metadata-scores-for-a-table /openapi/em-data-readiness-v2.yaml delete /api/data-readiness/data-assets/tables/scores Clear one or both score types for a table in a single call. Use scope=data_quality to clear DQ tags and rules, scope=metadata to clear the metadata score, or scope=all to clear everything (the Clear All action). Previous data is preserved for history. Running a new assessment restores the score. # Get a column by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-a-column-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/columns/by-fqn Get a specific column by its fully qualified name (FQN). The FQN format is: service.database.schema.table.column Example: PostgreSQL.dvdrental.public.customer.email Args: fqn: Fully qualified name of the column. ctx: Request context for authentication. service: Data assets service instance. Returns: Column domain model. Raises: HTTPException: 404 if column not found. # Get a database by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-a-database-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/databases/by-fqn Get a specific database by its fully qualified name (FQN). Args: fqn: Fully qualified name of the database. ctx: Request context for authentication. service: Data assets service instance. Returns: Database domain model. Raises: HTTPException: 404 if database not found. # Get a schema by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-a-schema-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/schemas/by-fqn Get a specific schema by its fully qualified name (FQN). The FQN format is: service.database.schema Example: PostgreSQL.dvdrental.public Args: fqn: Fully qualified name of the schema. ctx: Request context for authentication. service: Data assets service instance. workflow_asset_service: Workflow asset integration service instance. profile_service: Profile service for fetching profile data. include_tables: Whether to include tables in the schema (default: true). include_pending_workflow: Whether to include pending workflow info (default: false). include_profile: Whether to include profile summary per table (default: false). search: Filter tables by name or description (case-insensitive). Only applied when include_tables=true. Returns: Schema domain model with tables if requested, SchemaWithProfileResponse if include_profile is true, or SchemaWithWorkflowResponse if include_pending_workflow is true. Raises: HTTPException: 404 if schema not found. # Get a table by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-a-table-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/tables/by-fqn Get a specific table by its fully qualified name (FQN). The FQN format is: service.database.schema.table Example: PostgreSQL.dvdrental.public.customer Args: fqn: Fully qualified name of the table. ctx: Request context for authentication. service: Data assets service instance. workflow_asset_service: Workflow asset integration service instance. include_pending_workflow: Whether to include pending workflow info (default: false). include_dq: Whether to include DQ tags, rules, and executions (default: false). Returns: Table domain model with columns if requested, or TableWithWorkflowResponse if include_pending_workflow is true. Raises: HTTPException: 404 if table not found. # Get latest column profile by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-latest-column-profile-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/profiles/columns/by-fqn Get the most recent profiling data for a column by its fully qualified name. Args: fqn: Fully qualified name of the column. ctx: Request context for authentication. profile_service: Profile service instance. Returns: Latest column profile data. Raises: HTTPException: 404 if no profile found for the column. # Get latest table profile by FQN Source: https://docs.emergence.ai/api-reference/data-assets/get-latest-table-profile-by-fqn /openapi/em-data-readiness-v2.yaml get /api/data-readiness/data-assets/profiles/tables/by-fqn Get the most recent profiling data for a table by its fully qualified name. Args: fqn: Fully qualified name of the table. ctx: Request context for authentication. profile_service: Profile service instance. Returns: Latest table profile data. Raises: HTTPException: 404 if no profile found for the table. # Update DQ tags for a table Source: https://docs.emergence.ai/api-reference/data-assets/update-dq-tags-for-a-table /openapi/em-data-readiness-v2.yaml put /api/data-readiness/data-assets/tables/dq-tags Update (delete) DQ tags for columns in a table. This is a delete-only operation: for each column in the request, tags in the DB that are NOT in the request will be deleted. Columns not in the request are unchanged. Request tags not in DB are ignored. # Create Data Connection Source: https://docs.emergence.ai/api-reference/data_connections/create-data-connection /openapi/em-runtime-assets-v2.yaml post /assets/data Create data connection # Delete Data Connection Source: https://docs.emergence.ai/api-reference/data_connections/delete-data-connection /openapi/em-runtime-assets-v2.yaml delete /assets/data/{resource_uri} Delete data connection # Get Data Connection Source: https://docs.emergence.ai/api-reference/data_connections/get-data-connection /openapi/em-runtime-assets-v2.yaml get /assets/data/{resource_uri} Get data connection # Get Data Connection Secret Source: https://docs.emergence.ai/api-reference/data_connections/get-data-connection-secret /openapi/em-runtime-assets-v2.yaml get /assets/data/{resource_uri}/secret Get data connection secrets # List Connection Types Source: https://docs.emergence.ai/api-reference/data_connections/list-connection-types /openapi/em-runtime-assets-v2.yaml get /assets/data/types List all data connection types with their credential schemas # List Data Connections Source: https://docs.emergence.ai/api-reference/data_connections/list-data-connections /openapi/em-runtime-assets-v2.yaml get /assets/data Get data connections # Update Data Connection Source: https://docs.emergence.ai/api-reference/data_connections/update-data-connection /openapi/em-runtime-assets-v2.yaml put /assets/data/{resource_uri} Update data connection # Verify Connection Before Save Source: https://docs.emergence.ai/api-reference/data_connections/verify-connection-before-save /openapi/em-runtime-assets-v2.yaml post /assets/data/verify Verify data connection credentials before creating the connection # Verify Data Connection Source: https://docs.emergence.ai/api-reference/data_connections/verify-data-connection /openapi/em-runtime-assets-v2.yaml post /assets/data/{resource_uri}/verify Verify data connection credentials and accessibility # Send an HTML email to one or more recipients Source: https://docs.emergence.ai/api-reference/email/send-an-html-email-to-one-or-more-recipients /openapi/em-runtime-utils-v2.yaml post /utils/email Send an HTML email to one or more recipients. The `body` field is treated as HTML. # Generate an embedding vector Source: https://docs.emergence.ai/api-reference/embeddings/generate-an-embedding-vector /openapi/em-runtime-search-v2.yaml post /search/embeddings Generates an embedding vector for the provided text using the configured embedding provider (default: Google Gemini). Used by publishing services to embed entity text before indexing, and by the global search endpoint to vectorise natural language queries. # Error Codes Source: https://docs.emergence.ai/api-reference/errors Error response format, error codes, and troubleshooting guide for CRAFT APIs. # Error Codes All CRAFT APIs return consistent error responses. This page documents the error format, all error codes, and troubleshooting guidance for each. ## Error Response Format Error responses follow a standard JSON structure: ```json theme={null} { "error": { "code": "RESOURCE_NOT_FOUND", "message": "Agent with ID '550e8400-e29b-41d4-a716-446655440000' not found.", "status": 404, "details": { "resource_type": "artifact", "resource_id": "550e8400-e29b-41d4-a716-446655440000" }, "request_id": "req-a1b2c3d4-e5f6-7890-abcd-ef1234567890", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" } } ``` | Field | Type | Description | | ------------ | ------- | ----------------------------------------------- | | `code` | string | Machine-readable error code | | `message` | string | Human-readable error description | | `status` | integer | HTTP status code | | `details` | object | Additional context (varies by error type) | | `request_id` | string | Unique request identifier for support | | `trace_id` | string | OpenTelemetry trace ID for end-to-end debugging | ## HTTP Status Codes | Status | Meaning | When Returned | | ------ | --------------------- | --------------------------------------------------------------------- | | `400` | Bad Request | Invalid request body, missing required fields, malformed parameters | | `401` | Unauthorized | Missing, expired, or invalid JWT token | | `403` | Forbidden | Valid token but insufficient permissions | | `404` | Not Found | Resource does not exist or is not accessible | | `409` | Conflict | Resource already exists or version conflict | | `412` | Precondition Failed | `If-Match` provided but does not match current resource version (PUT) | | `413` | Payload Too Large | Uploaded body exceeds the server's size limit (artifact upload) | | `422` | Unprocessable Entity | Request is well-formed but semantically invalid | | `428` | Precondition Required | PUT without `If-Match` header (concurrency control required) | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Internal Server Error | Unexpected server error | | `502` | Bad Gateway | Upstream service (Keycloak, OpenFGA) unavailable | | `503` | Service Unavailable | Service is starting up or shutting down | ## Error Codes Reference The OpenAPI specifications define a base `ErrorCode` enum with 9 values: `INVALID_REQUEST`, `INVALID_TOKEN`, `INSUFFICIENT_PERMISSIONS`, `RESOURCE_NOT_FOUND`, `RESOURCE_ALREADY_EXISTS`, `VALIDATION_ERROR`, `RATE_LIMIT_EXCEEDED`, `SERVICE_UNAVAILABLE`, `INTERNAL_ERROR`. The implementation may return more specific codes within each category as documented below. ### Authentication Errors (401) | Code | Message | Troubleshooting | | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------- | | `UNAUTHENTICATED` | No authentication token provided | Include `Authorization: Bearer ` header | | `TOKEN_EXPIRED` | Access token has expired | Refresh the token or obtain a new one | | `TOKEN_INVALID` | Token signature verification failed | Ensure the token was issued by the correct Keycloak realm | | `TOKEN_MALFORMED` | Token format is invalid | Check that the token is a valid JWT (three base64-encoded segments separated by dots) | ### Authorization Errors (403) | Code | Message | Troubleshooting | | ----------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `FORBIDDEN` | Insufficient permissions for this operation | Check your role assignment on the target project; see [RBAC Configuration](/guides/rbac-configuration) | | `PROJECT_ACCESS_DENIED` | No access to the specified project | Verify the project ID in the request matches a project you have been granted access to | | `ORG_ACCESS_DENIED` | Token does not belong to this organization | Authenticate against the correct Keycloak realm for the target organization | ### Resource Errors (404, 409) | Code | Message | Troubleshooting | | ------------------------- | --------------------------------------------- | --------------------------------------------------------------------- | | `RESOURCE_NOT_FOUND` | Resource not found | Verify the resource ID and ensure you have `can_read` permission | | `RESOURCE_ALREADY_EXISTS` | Resource with this identifier already exists | Use a unique name or identifier; use PUT to update existing resources | | `VERSION_CONFLICT` | Resource has been modified by another request | Re-fetch the resource and retry with the current version | ### Validation Errors (400, 422) | Code | Message | Troubleshooting | | ------------------------ | ------------------------------ | ----------------------------------------------------------------- | | `VALIDATION_ERROR` | Request body validation failed | Check the `details` field for specific field-level errors | | `INVALID_PARAMETER` | Invalid query parameter | Verify parameter name, type, and allowed values | | `MISSING_REQUIRED_FIELD` | Required field is missing | Include all required fields in the request body | | `INVALID_FORMAT` | Field value format is invalid | Check the expected format (UUID, URL, email, etc.) | | `SEARCH_QUERY_INVALID` | Malformed search query | Use plain text for search queries; special operators are reserved | ### Rate Limiting Errors (429) | Code | Message | Troubleshooting | | --------------------- | --------------------- | --------------------------------------------------------------------------- | | `RATE_LIMIT_EXCEEDED` | Rate limit exceeded | Wait for the `Retry-After` header duration; implement exponential backoff | | `QUOTA_EXCEEDED` | Tenant quota exceeded | Contact your administrator to increase the quota for the exceeded dimension | ### Server Errors (500, 502, 503) | Code | Message | Troubleshooting | | ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- | | `INTERNAL_ERROR` | An unexpected error occurred | Retry the request; if persistent, report with `request_id` and `trace_id` | | `UPSTREAM_UNAVAILABLE` | Upstream service is unavailable | Keycloak or OpenFGA may be down; check service health. Governance down causes Assets/Utils to return 403 | | `SERVICE_STARTING` | Service is initializing | Wait for the service to complete startup; check readiness probe | | `DATABASE_ERROR` | Database operation failed | Retry the request; if persistent, check database connectivity | ## Handling Errors in Code ```python theme={null} from em_runtime_assets_sdk import AuthenticatedClient from em_runtime_assets_sdk.api.artifacts import get_artifact from em_runtime_assets_sdk.errors import UnexpectedStatus client = AuthenticatedClient( base_url="https:///api/assets", token=token, ) try: agent = await get_artifact.asyncio(client=client, artifact_uri=artifact_uri) except UnexpectedStatus as e: if e.status_code == 401: # Token expired -- refresh and rebuild the client (token is immutable). token = await refresh_token() client = AuthenticatedClient( base_url="https:///api/assets", token=token ) agent = await get_artifact.asyncio(client=client, artifact_uri=artifact_uri) elif e.status_code == 404: print(f"Not found: {e.content!r}") elif e.status_code == 429: # Rate limited -- read Retry-After from response headers retry_after = int(e.response.headers.get("Retry-After", "1")) await asyncio.sleep(retry_after) agent = await get_artifact.asyncio(client=client, artifact_uri=artifact_uri) else: print(f"HTTP {e.status_code}: {e.content!r}") raise ``` `UnexpectedStatus.content` is raw `bytes`; decode and `json.loads()` to read the envelope (`{"error": {"code", "message", "request_id"}}`). ```typescript theme={null} import { Client, ApiError } from '@emergence-ai/em-runtime-assets-sdk'; import { getArtifact } from '@emergence-ai/em-runtime-assets-sdk/api'; const client = new Client({ BASE: 'https:///api/assets', TOKEN: accessToken, }); try { const agent = await getArtifact({ client, artifactUri }); } catch (error) { if (error instanceof ApiError) { const body = error.body as | { error?: { code?: string; message?: string; request_id?: string } } | undefined; switch (error.status) { case 401: // Token expired -- refresh and rebuild the client break; case 404: console.log(`Not found: ${body?.error?.message}`); break; case 429: { const retryAfter = Number(error.headers?.['retry-after'] ?? '1'); await sleep(retryAfter * 1000); break; } default: console.error(`HTTP ${error.status} (${body?.error?.code}): ${body?.error?.message}`); console.error(`Request ID: ${body?.error?.request_id}`); } } } ``` ```bash theme={null} # Check the HTTP status code HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer $TOKEN" \ "https:///api/assets/artifacts/$ARTIFACT_URI") if [ "$HTTP_STATUS" -eq 401 ]; then echo "Token expired, refreshing..." TOKEN=$(refresh_token) elif [ "$HTTP_STATUS" -eq 404 ]; then echo "Agent not found" elif [ "$HTTP_STATUS" -eq 429 ]; then echo "Rate limited, waiting..." sleep 60 fi ``` ## Retry Strategy For transient errors (429, 500, 502, 503), implement exponential backoff with jitter: | Attempt | Wait Time | Max Wait | | ----------- | ------------------ | ---------- | | 1st retry | 1 second + jitter | 2 seconds | | 2nd retry | 2 seconds + jitter | 4 seconds | | 3rd retry | 4 seconds + jitter | 8 seconds | | 4th retry | 8 seconds + jitter | 16 seconds | | Max retries | 5 | 30 seconds | For 429 errors, always respect the `Retry-After` header rather than using exponential backoff. The header provides the exact wait time until the rate limit window resets. ## Getting Help When reporting an issue, include: 1. The `request_id` from the error response 2. The `trace_id` for end-to-end correlation 3. The timestamp of the request 4. The full error response body ## Next Steps Review the full API structure and endpoints. Learn how to obtain and manage JWT tokens. Use the Python SDK with built-in error handling. Use the TypeScript SDK with built-in error handling. # Delete File Source: https://docs.emergence.ai/api-reference/files/delete-file /openapi/em-runtime-assets-v2.yaml delete /assets/files/{resource_uri} Delete file # Download file Source: https://docs.emergence.ai/api-reference/files/download-file /openapi/em-runtime-assets-v2.yaml get /assets/files/{resource_uri}/download Download a file from object storage # Get File Source: https://docs.emergence.ai/api-reference/files/get-file /openapi/em-runtime-assets-v2.yaml get /assets/files/{resource_uri} Get file details # List Files Source: https://docs.emergence.ai/api-reference/files/list-files /openapi/em-runtime-assets-v2.yaml get /assets/files List files in project # Update File Source: https://docs.emergence.ai/api-reference/files/update-file /openapi/em-runtime-assets-v2.yaml put /assets/files/{resource_uri} Update file metadata # Upload file Source: https://docs.emergence.ai/api-reference/files/upload-file /openapi/em-runtime-assets-v2.yaml post /assets/files Upload a file to object storage and create file record # Bulk upsert FK edges Source: https://docs.emergence.ai/api-reference/fk-graph/bulk-upsert-fk-edges /openapi/em-runtime-utils-v2.yaml put /utils/{resource_type}/{resource_uri}/fk-edges Create or update FK edges for a resource. Existing edges are updated, new edges are created. # Delete FK edges Source: https://docs.emergence.ai/api-reference/fk-graph/delete-fk-edges /openapi/em-runtime-utils-v2.yaml delete /utils/{resource_type}/{resource_uri}/fk-edges Delete all FK edges for a resource. # Expand table selection via FK relationships Source: https://docs.emergence.ai/api-reference/fk-graph/expand-table-selection-via-fk-relationships /openapi/em-runtime-utils-v2.yaml post /utils/{resource_type}/{resource_uri}/fk-edges/expand Given a list of table FQNs, discover related tables via FK traversal (BFS). # Find shortest paths between tables Source: https://docs.emergence.ai/api-reference/fk-graph/find-shortest-paths-between-tables /openapi/em-runtime-utils-v2.yaml post /utils/{resource_type}/{resource_uri}/fk-edges/find-paths Find shortest FK paths between all pairs of input tables. # List FK edges Source: https://docs.emergence.ai/api-reference/fk-graph/list-fk-edges /openapi/em-runtime-utils-v2.yaml get /utils/{resource_type}/{resource_uri}/fk-edges List FK edges for a resource, optionally filtered by table FQN. # Add a tag to a glossary term Source: https://docs.emergence.ai/api-reference/glossary/add-a-tag-to-a-glossary-term /openapi/em-data-readiness-v2.yaml post /api/data-readiness/glossary/terms/{term_id}/tags Add a tag to a glossary term. # Create a glossary term Source: https://docs.emergence.ai/api-reference/glossary/create-a-glossary-term /openapi/em-data-readiness-v2.yaml post /api/data-readiness/glossary/terms Create a new glossary term. # Delete a glossary term Source: https://docs.emergence.ai/api-reference/glossary/delete-a-glossary-term /openapi/em-data-readiness-v2.yaml delete /api/data-readiness/glossary/terms/{term_id} Soft-delete a glossary term. # Get a glossary term by ID Source: https://docs.emergence.ai/api-reference/glossary/get-a-glossary-term-by-id /openapi/em-data-readiness-v2.yaml get /api/data-readiness/glossary/terms/{term_id} Get a single glossary term by its ID. # Get alphabet index with term counts Source: https://docs.emergence.ai/api-reference/glossary/get-alphabet-index-with-term-counts /openapi/em-data-readiness-v2.yaml get /api/data-readiness/glossary/terms/index Return term counts grouped by first letter for the A-Z index navigation. # List glossary terms Source: https://docs.emergence.ai/api-reference/glossary/list-glossary-terms /openapi/em-data-readiness-v2.yaml get /api/data-readiness/glossary/terms List glossary terms across all DB connections with optional filters. # Update a glossary term Source: https://docs.emergence.ai/api-reference/glossary/update-a-glossary-term /openapi/em-data-readiness-v2.yaml patch /api/data-readiness/glossary/terms/{term_id} Update an existing glossary term (partial update). # Verify a glossary term Source: https://docs.emergence.ai/api-reference/glossary/verify-a-glossary-term /openapi/em-data-readiness-v2.yaml post /api/data-readiness/glossary/terms/{term_id}/verify Verify a glossary term (sets status to Verified). # Verify multiple glossary terms Source: https://docs.emergence.ai/api-reference/glossary/verify-multiple-glossary-terms /openapi/em-data-readiness-v2.yaml post /api/data-readiness/glossary/terms/bulk-verify Verify multiple glossary terms at once. # Health Check Source: https://docs.emergence.ai/api-reference/health-check /openapi/em-talk2data-v2.yaml get /talk2data/health # Health Source: https://docs.emergence.ai/api-reference/health/health /openapi/em-runtime-search-v2.yaml get /health Health check endpoint. # Health Check Source: https://docs.emergence.ai/api-reference/health/health-check /openapi/em-data-readiness-v2.yaml get /api/data-readiness/health # Service Info Source: https://docs.emergence.ai/api-reference/info/service-info /openapi/em-runtime-search-v2.yaml get /search/ Service information endpoint. # API Overview Source: https://docs.emergence.ai/api-reference/introduction Overview of CRAFT REST APIs including base URLs, versioning, rate limits, and service endpoints for Governance, Assets, and Utils. # API Overview CRAFT exposes RESTful APIs through five services: **Governance** (identity and permissions), **Assets** (data connections and files), **Utils** (scheduling, context packs, and memories), **Talk2Data** (conversational data analytics), and **Data Readiness** (data profiling and governance). All APIs follow consistent conventions for authentication, versioning, and error handling. ## Base URLs Each platform service runs on a dedicated internal port. In production, services are accessed via path-based routing through a Gateway: | Service | Internal Port | Path Prefix | Responsibility | | ------------------ | ------------- | ----------------- | --------------------------------------------- | | **Governance** | 8000 | `/governance` | Organizations, projects, permissions, secrets | | **Assets** | 8000 | `/assets` | Artifacts, data connections, files | | **Utils** | 8000 | `/utils` | Scheduling, context packs, memories, metadata | | **Talk2Data** | 8080 | `/talk2data` | Chat sessions, NL-to-SQL, data analysis | | **Data Readiness** | 8000 | `/data-readiness` | Profiling workflows, data quality, glossary | The URLs above show direct service access. In production deployments behind an ingress controller, services are exposed on a single domain with path-based routing (e.g., `https://api.example.com/governance`, `https://api.example.com/assets`). The ingress rewrites paths to the service-internal prefix. ## API Versioning CRAFT APIs use service-specific prefixes (/governance, /assets, /utils). The platform follows these versioning principles: * **Path-based versioning**: Each service uses its own prefix (e.g., `/governance/...`, `/assets/...`, `/utils/...`) * **Backward compatibility**: Minor additions (new fields, new endpoints) are added within the same version * **Breaking changes**: Changes that remove fields, rename endpoints, or alter behavior require a new version (`/v2`) * **Deprecation**: Deprecated versions are maintained for at least 6 months with `Sunset` headers ## Common Headers ### Required Headers | Header | Description | Example | | --------------- | ------------------------------------------------------------------- | ------------------ | | `Authorization` | Bearer token for authentication (JWT contains `org_id` for scoping) | `Bearer eyJhbG...` | | `Content-Type` | Request body format | `application/json` | ### Response Headers | Header | Description | Example | | ----------------------- | ------------------------------------- | -------------- | | `X-Request-ID` | Unique request identifier for tracing | `req-a1b2c3d4` | | `X-RateLimit-Limit` | Maximum requests allowed in window | `300` | | `X-RateLimit-Remaining` | Requests remaining in current window | `297` | | `X-RateLimit-Reset` | Unix timestamp when window resets | `1712150400` | ## Rate Limits Rate limits are enforced per authenticated principal using a sliding window algorithm backed by Redis: | Operation Type | Limit | Applies To | | ----------------------------------------------- | ------- | --------------------------------------- | | **Read operations** (GET, POST :search) | 300/min | Authenticated users | | **Write operations** (POST, PUT, PATCH, DELETE) | 60/min | Authenticated users | | **Bulk operations** | 10/min | Authenticated users | | **Anonymous reads** | 30/min | Unauthenticated requests (when enabled) | When a rate limit is exceeded, the API returns: ```json theme={null} { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Retry after 42 seconds.", "status": 429 } } ``` The `Retry-After` response header indicates when the client can retry. Rate limits are configurable per tenant by administrators. Contact your platform admin if the default limits are insufficient for your workload. ## Service Endpoints Summary ### Governance API | Method | Endpoint | Description | | ------ | ------------------------------------ | -------------------------------- | | GET | `/governance/organizations` | List organizations | | GET | `/governance/organizations/{org_id}` | Get organization details | | POST | `/governance/projects` | Create a project | | GET | `/governance/projects` | List projects | | GET | `/governance/projects/{project_id}` | Get project details | | GET | `/governance/permissions/check` | Check if a user has a permission | | POST | `/governance/permissions/grant` | Grant a permission | | POST | `/governance/permissions/revoke` | Revoke a permission | | \* | `/governance/resources/.../secrets` | Secret management (CRUD) | ### Assets API | Method | Endpoint | Description | | ------ | ------------------------------------------- | --------------------------- | | POST | `/assets/artifacts` | Create an artifact | | GET | `/assets/artifacts/{resource_uri}` | Get artifact details | | GET | `/assets/artifacts/{resource_uri}/preview` | Preview artifact content | | GET | `/assets/artifacts/{resource_uri}/download` | Download artifact | | POST | `/assets/data` | Register a data connection | | GET | `/assets/data/{resource_uri}` | Get data connection details | | POST | `/assets/data/{resource_uri}/verify` | Verify a data connection | | POST | `/assets/files` | Upload a file | | GET | `/assets/files/{resource_uri}` | Get file metadata | | GET | `/assets/files/{resource_uri}/download` | Download file | ### Utils API | Method | Endpoint | Description | | ------ | ------------------------------------------------ | -------------------------- | | POST | `/utils/schedules` | Create a schedule | | GET | `/utils/schedules/{schedule_id}` | Get schedule details | | PUT | `/utils/schedules/{schedule_id}` | Update a schedule | | DELETE | `/utils/schedules/{schedule_id}` | Delete a schedule | | POST | `/utils/context-packs` | Create a context pack | | GET | `/utils/context-packs` | List context packs | | POST | `/utils/memories` | Create a memory | | GET | `/utils/memories` | List memories | | POST | `/utils/search/metadata` | Search metadata catalog | | \* | `/utils/{resource_type}/{resource_uri}/metadata` | Metadata management (CRUD) | ### Talk2Data API (Data Insights) | Method | Endpoint | Description | | ------ | ----------------------------------------------------------------------- | ----------------------- | | POST | `/talk2data/chat/sessions` | Create a chat session | | GET | `/talk2data/chat/sessions` | List chat sessions | | GET | `/talk2data/chat/sessions/{session_id}` | Get session details | | PATCH | `/talk2data/chat/sessions/{session_id}` | Rename a session | | DELETE | `/talk2data/chat/sessions/{session_id}` | Delete a session | | POST | `/talk2data/chat/sessions/{session_id}/turns` | Create a new turn | | GET | `/talk2data/chat/sessions/{session_id}/turns` | List turns in a session | | GET | `/talk2data/chat/sessions/{session_id}/turns/{turn_id}` | Get turn details | | GET | `/talk2data/chat/sessions/{session_id}/turns/{turn_id}/artifacts` | Get turn artifacts | | POST | `/talk2data/chat/sessions/{session_id}/turns/{turn_id}/feedback` | Submit feedback | | POST | `/talk2data/chat/messages/sessions/{session_id}/turns/{turn_id}/stream` | Stream message (SSE) | | POST | `/talk2data/chat/messages/{task_id}/cancel` | Cancel a running task | ### Data Readiness API (Data Governance) | Method | Endpoint | Description | | ------ | --------------------------------------------------- | ------------------------------- | | GET | `/data-readiness/workflows` | List workflow configurations | | POST | `/data-readiness/workflows` | Create a workflow configuration | | GET | `/data-readiness/workflows/{configuration_id}` | Get workflow details | | POST | `/data-readiness/workflows/{configuration_id}/runs` | Trigger a workflow run | | GET | `/data-readiness/workflows/{configuration_id}/runs` | List runs for a workflow | | \* | `/data-readiness/data-assets/...` | Data asset catalog management | | \* | `/data-readiness/metrics/...` | Quality metrics and scorecards | | \* | `/data-readiness/violations` | Data quality violation tracking | | \* | `/data-readiness/glossary/terms` | Business glossary management | ## OpenAPI Specifications Each service generates an OpenAPI 3.0 specification accessible at: * Governance: `https:///api/governance/docs` (Swagger UI) or `https:///api/governance/openapi.json` * Assets: `https:///api/assets/docs` or `https:///api/assets/openapi.json` * Utils: `https:///api/utils/docs` or `https:///api/utils/openapi.json` The OpenAPI specs are used to auto-generate Python and TypeScript SDKs. ## SDKs Auto-generated SDKs are available for programmatic access: ```bash theme={null} pip install em-runtime-governance-sdk-python pip install em-runtime-assets-sdk-python pip install em-runtime-utils-sdk-python ``` ```python theme={null} from em_runtime_governance_sdk import AuthenticatedClient from em_runtime_governance_sdk.api.organizations import list_organizations client = AuthenticatedClient( base_url="https:///api/governance", token=access_token, ) orgs = await list_organizations.asyncio(client=client) ``` ```bash theme={null} npm install @emergence-ai/em-runtime-governance-sdk npm install @emergence-ai/em-runtime-assets-sdk npm install @emergence-ai/em-runtime-utils-sdk ``` ```typescript theme={null} import { Client } from '@emergence-ai/em-runtime-governance-sdk'; import { listOrganizations } from '@emergence-ai/em-runtime-governance-sdk/api'; const client = new Client({ BASE: 'https:///api/governance', TOKEN: accessToken, }); const orgs = await listOrganizations({ client }); ``` ## Pagination List endpoints support offset-based pagination using `page` and `limit` parameters: ```bash theme={null} # First page GET /assets/artifacts?limit=20&page=1 # Next page GET /assets/artifacts?limit=20&page=2 ``` The `limit` parameter controls page size (default varies by endpoint). The `page` parameter is 1-indexed. ## Next Steps Learn how to obtain and use JWT tokens for API access. Understand error response formats and troubleshooting. Use the auto-generated Python SDK for programmatic access. Use the auto-generated TypeScript SDK for frontend integration. # Execute Query Endpoint Source: https://docs.emergence.ai/api-reference/mcp/execute-query-endpoint /openapi/em-talk2data-v2.yaml post /talk2data/mcp/execute-query Execute a read-only SELECT SQL statement against a configured data connection and return the result rows. Typically called after `generate_sql`; pass the returned `rows` field directly into `generate_plotly_chart` to visualise the result. # Generate Plotly Chart Endpoint Source: https://docs.emergence.ai/api-reference/mcp/generate-plotly-chart-endpoint /openapi/em-talk2data-v2.yaml post /talk2data/mcp/generate-plotly-chart Visualise tabular results from `execute_query` as an interactive Plotly chart. Pass the `rows` from `execute_query` directly as `data`. Supports 25+ chart types (bar, line, heatmap, treemap, sankey, waterfall, parcoords, choropleth, etc.). Pick the chart type based on data shape: bar/line for categorical-vs-numeric, heatmap for matrix, treemap/sunburst for hierarchy, choropleth for geographic, sankey/funnel for flow. # Generate Sql Source: https://docs.emergence.ai/api-reference/mcp/generate-sql /openapi/em-talk2data-v2.yaml post /talk2data/mcp/generate-sql Generate SQL from a natural-language question. Returns the SQL string plus the schemas it references. Pipe the SQL into `execute_query` to fetch rows; then feed those rows into `generate_plotly_chart` to render a figure. Use this as the first step in any 'show me X' or 'what is Y' data-analysis flow. # Resolve Term Source: https://docs.emergence.ai/api-reference/mcp/resolve-term /openapi/em-talk2data-v2.yaml post /talk2data/mcp/resolve-term Resolve a business term against context pack memories. Fetches all memories from the MemoryProvider and filters client-side by term match (name or content contains the search term). Extracts structured fields (definition, related_columns, sql_pattern) from the best-matching concept/vocabulary/sample_question memories. Example: "What does 'conversion rate' mean?" will return a definition from concept/vocabulary memories, related column names from table description memories, and an example SQL pattern from sample_question memories. # Archive a memory Source: https://docs.emergence.ai/api-reference/memories/archive-a-memory /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/archive Archive a memory. # Archive Memory Source: https://docs.emergence.ai/api-reference/memories/archive-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/archive # Get a memory by ID Source: https://docs.emergence.ai/api-reference/memories/get-a-memory-by-id /openapi/em-runtime-utils-v2.yaml get /utils/memories/{memory_id} Get a memory by ID. # Get Memory By Id Source: https://docs.emergence.ai/api-reference/memories/get-memory-by-id /openapi/em-memory-service-v2.yaml get /memories/{memory_id} # List Memories Source: https://docs.emergence.ai/api-reference/memories/list-memories /openapi/em-memory-service-v2.yaml get /memories # Mark a memory as superseded Source: https://docs.emergence.ai/api-reference/memories/mark-a-memory-as-superseded /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/supersede/{superseded_by} Mark a memory as superseded by another memory. # Permanent Delete Memory Source: https://docs.emergence.ai/api-reference/memories/permanent-delete-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/permanent-delete # Permanently delete a memory Source: https://docs.emergence.ai/api-reference/memories/permanently-delete-a-memory /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/permanent-delete Permanently delete a memory. # Pin a memory Source: https://docs.emergence.ai/api-reference/memories/pin-a-memory /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/pin Pin a memory to mark it as important. # Pin Memory Source: https://docs.emergence.ai/api-reference/memories/pin-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/pin # Soft delete a memory Source: https://docs.emergence.ai/api-reference/memories/soft-delete-a-memory /openapi/em-runtime-utils-v2.yaml delete /utils/memories/{memory_id} Soft delete a memory (can be recovered). # Soft Delete Memory Source: https://docs.emergence.ai/api-reference/memories/soft-delete-memory /openapi/em-memory-service-v2.yaml delete /memories/{memory_id} # Supersede Memory Source: https://docs.emergence.ai/api-reference/memories/supersede-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/supersede/{superseded_by} # Unpin a memory Source: https://docs.emergence.ai/api-reference/memories/unpin-a-memory /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/unpin Unpin a memory. # Unpin Memory Source: https://docs.emergence.ai/api-reference/memories/unpin-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/unpin # Update a memory Source: https://docs.emergence.ai/api-reference/memories/update-a-memory /openapi/em-runtime-utils-v2.yaml patch /utils/memories/{memory_id} Update a memory's content, classification, or metadata. # Update Memory Source: https://docs.emergence.ai/api-reference/memories/update-memory /openapi/em-memory-service-v2.yaml patch /memories/{memory_id} # Validate a memory Source: https://docs.emergence.ai/api-reference/memories/validate-a-memory /openapi/em-runtime-utils-v2.yaml post /utils/memories/{memory_id}/validate Validate a memory with a confidence score. # Validate Memory Source: https://docs.emergence.ai/api-reference/memories/validate-memory /openapi/em-memory-service-v2.yaml post /memories/{memory_id}/validate # Bulk get metadata by FQN Source: https://docs.emergence.ai/api-reference/metadata/bulk-get-metadata-by-fqn /openapi/em-runtime-utils-v2.yaml post /utils/metadata/get Retrieve multiple metadata entities by FQN. Always returns the latest version. # Bulk get tags by FQN Source: https://docs.emergence.ai/api-reference/metadata/bulk-get-tags-by-fqn /openapi/em-runtime-utils-v2.yaml post /utils/metadata/tags/get Retrieve multiple tags by FQN. Always returns the latest non-deleted version. # Bulk soft-delete metadata Source: https://docs.emergence.ai/api-reference/metadata/bulk-soft-delete-metadata /openapi/em-runtime-utils-v2.yaml post /utils/metadata/delete Soft-delete multiple metadata entities with cascade. # Bulk soft-delete tags Source: https://docs.emergence.ai/api-reference/metadata/bulk-soft-delete-tags /openapi/em-runtime-utils-v2.yaml post /utils/metadata/tags/delete Soft-delete multiple tags by FQN. Not-found items are non-fatal. # Bulk store (upsert) metadata Source: https://docs.emergence.ai/api-reference/metadata/bulk-store-upsert-metadata /openapi/em-runtime-utils-v2.yaml post /utils/metadata/store Create or update multiple metadata entities. Items are sorted by FQN depth so ancestors are processed first. # Bulk store (upsert) tags Source: https://docs.emergence.ai/api-reference/metadata/bulk-store-upsert-tags /openapi/em-runtime-utils-v2.yaml post /utils/metadata/tags/store Create or update multiple tags. Parent categories must exist. # Clear enrichment fields on child entities Source: https://docs.emergence.ai/api-reference/metadata/clear-enrichment-fields-on-child-entities /openapi/em-runtime-utils-v2.yaml post /utils/{resource_type}/{resource_uri}/metadata/clear-enrichments Clear enrichment fields on all direct children of the given FQN. For a schema FQN: clears description and tags on all tables. For a table FQN: clears description, extension, business_definition, business_rules, enrichment flags, and tags on all columns. # Create or update a catalog tag Source: https://docs.emergence.ai/api-reference/metadata/create-or-update-a-catalog-tag /openapi/em-runtime-utils-v2.yaml post /utils/metadata/tags Upsert a catalog tag. Parent category must exist and not be deleted. Returns 201 on create, 200 on update. # Create or update a catalog tag category Source: https://docs.emergence.ai/api-reference/metadata/create-or-update-a-catalog-tag-category /openapi/em-runtime-utils-v2.yaml post /utils/metadata/catalog-tag-categories Upsert a catalog tag category. Returns 201 on create, 200 on update. # Delete a catalog tag Source: https://docs.emergence.ai/api-reference/metadata/delete-a-catalog-tag /openapi/em-runtime-utils-v2.yaml delete /utils/metadata/tags Soft-delete a catalog tag. # Delete a catalog tag category Source: https://docs.emergence.ai/api-reference/metadata/delete-a-catalog-tag-category /openapi/em-runtime-utils-v2.yaml delete /utils/metadata/catalog-tag-categories Soft-delete a catalog tag category and cascade to child tags. # Get metadata by FQN Source: https://docs.emergence.ai/api-reference/metadata/get-metadata-by-fqn /openapi/em-runtime-utils-v2.yaml get /utils/{resource_type}/{resource_uri}/metadata Retrieve a metadata asset by fully qualified name. Optionally filter by asset_type or retrieve a specific historical version. # Get or list catalog tag categories Source: https://docs.emergence.ai/api-reference/metadata/get-or-list-catalog-tag-categories /openapi/em-runtime-utils-v2.yaml get /utils/metadata/catalog-tag-categories If 'fqn' is provided, returns a single catalog tag category. Otherwise, returns a paginated list. # Get or list catalog tags Source: https://docs.emergence.ai/api-reference/metadata/get-or-list-catalog-tags /openapi/em-runtime-utils-v2.yaml get /utils/metadata/tags If 'fqn' is provided, returns a single tag. Otherwise, returns a paginated list. # List metadata entities Source: https://docs.emergence.ai/api-reference/metadata/list-metadata-entities /openapi/em-runtime-utils-v2.yaml get /utils/{resource_type}/{resource_uri}/metadata/query Returns a paginated list of metadata entities under the resource, with filtering and sorting. # List version history for an entity Source: https://docs.emergence.ai/api-reference/metadata/list-version-history-for-an-entity /openapi/em-runtime-utils-v2.yaml get /utils/{resource_type}/{resource_uri}/metadata/versions Returns enriched version history with structured diffs, per-field attribution, and contributor list. # Soft-delete metadata by FQN Source: https://docs.emergence.ai/api-reference/metadata/soft-delete-metadata-by-fqn /openapi/em-runtime-utils-v2.yaml delete /utils/{resource_type}/{resource_uri}/metadata Soft-delete a metadata entity (all versions + cascade) or a single version row. # Store (upsert) metadata Source: https://docs.emergence.ai/api-reference/metadata/store-upsert-metadata /openapi/em-runtime-utils-v2.yaml post /utils/{resource_type}/{resource_uri}/metadata Create or update a metadata asset. The asset type is determined by the type discriminator field in the request body. # Get Aggregated Scorecard Source: https://docs.emergence.ai/api-reference/metrics/get-aggregated-scorecard /openapi/em-data-readiness-v2.yaml post /api/data-readiness/metrics/scorecard Returns aggregated metric scores at the requested scope level. # Get Filter Options Source: https://docs.emergence.ai/api-reference/metrics/get-filter-options /openapi/em-data-readiness-v2.yaml get /api/data-readiness/metrics/filter-options Returns distinct filter values for cascading dashboard dropdowns, scoped by assessment type. # Get Per-Asset Metric Details Source: https://docs.emergence.ai/api-reference/metrics/get-per-asset-metric-details /openapi/em-data-readiness-v2.yaml get /api/data-readiness/metrics/details Returns per-asset metric detail for drill-down analysis. # Get Score Trend Source: https://docs.emergence.ai/api-reference/metrics/get-score-trend /openapi/em-data-readiness-v2.yaml get /api/data-readiness/metrics/trend Returns score trend over a time range, bucketed by the requested granularity. # List Dimensions Source: https://docs.emergence.ai/api-reference/metrics/list-dimensions /openapi/em-data-readiness-v2.yaml get /api/data-readiness/metrics/dimensions Returns dimension definitions with their metric definitions. # Create Model Source: https://docs.emergence.ai/api-reference/models/create-model /openapi/em-runtime-assets-v2.yaml post /assets/models Create model # Delete Model Source: https://docs.emergence.ai/api-reference/models/delete-model /openapi/em-runtime-assets-v2.yaml delete /assets/models/{resource_uri} Delete model # Get Model Source: https://docs.emergence.ai/api-reference/models/get-model /openapi/em-runtime-assets-v2.yaml get /assets/models/{resource_uri} Get model # Get Model Secret Source: https://docs.emergence.ai/api-reference/models/get-model-secret /openapi/em-runtime-assets-v2.yaml get /assets/models/{resource_uri}/secret Get model secrets # List Models Source: https://docs.emergence.ai/api-reference/models/list-models /openapi/em-runtime-assets-v2.yaml get /assets/models List models in project # Update Model Source: https://docs.emergence.ai/api-reference/models/update-model /openapi/em-runtime-assets-v2.yaml put /assets/models/{resource_uri} Update model # Verify Model Source: https://docs.emergence.ai/api-reference/models/verify-model /openapi/em-runtime-assets-v2.yaml post /assets/models/{resource_uri}/verify Verify model credentials and accessibility # Verify Model Before Save Source: https://docs.emergence.ai/api-reference/models/verify-model-before-save /openapi/em-runtime-assets-v2.yaml post /assets/models/verify Verify model credentials before creating the model # Create a new organization Source: https://docs.emergence.ai/api-reference/organizations/create-a-new-organization /openapi/em-runtime-governance-v2.yaml post /governance/organizations Creates a new tenant organization in the hub Keycloak realm (server-generated UUID), default organization groups, a database record, and OpenFGA permissions. Display `name` is provided by the caller; Keycloak assigns `id` and `alias`. Automatically creates default organization groups and sets up permissions in OpenFGA. # Get organization details Source: https://docs.emergence.ai/api-reference/organizations/get-organization-details /openapi/em-runtime-governance-v2.yaml get /governance/organizations/{org_id} Retrieves detailed information about a specific organization by its Keycloak UUID. The user must have permission to view the organization. Returns database metadata plus the Keycloak organization alias (for OIDC org switching). # List organizations for the current user Source: https://docs.emergence.ai/api-reference/organizations/list-organizations-for-the-current-user /openapi/em-runtime-governance-v2.yaml get /governance/organizations Returns Keycloak organizations the authenticated user belongs to in the hub realm. Each entry includes the organization UUID (`id`), display `name`, and Keycloak `alias` (for OIDC `scope=organization:` in the runtime-ui org switcher). # Check user permission Source: https://docs.emergence.ai/api-reference/permissions/check-user-permission /openapi/em-runtime-governance-v2.yaml get /governance/permissions/check Verifies if the authenticated user has permission to perform a specific action on a resource. Returns 200 if allowed, 403 if denied. # Delete all permissions for a resource Source: https://docs.emergence.ai/api-reference/permissions/delete-all-permissions-for-a-resource /openapi/em-runtime-governance-v2.yaml post /governance/permissions/delete-all Removes all permission tuples associated with a specific resource. Typically used during resource cleanup or deletion. Returns the count of deleted permissions. # Grant permission to user or group Source: https://docs.emergence.ai/api-reference/permissions/grant-permission-to-user-or-group /openapi/em-runtime-governance-v2.yaml post /governance/permissions/grant Grants a specific permission relation to a user or group on a resource. Used to assign roles like owner, admin, developer, operator, or viewer. # List all objects the user can read Source: https://docs.emergence.ai/api-reference/permissions/list-all-objects-the-user-can-read /openapi/em-runtime-governance-v2.yaml get /governance/permissions/accessible-objects Returns all object IDs across all resource types that the authenticated user has can_read on. Fans out to OpenFGA internally — callers make one HTTP request instead of one per resource type. # Revoke permission from user or group Source: https://docs.emergence.ai/api-reference/permissions/revoke-permission-from-user-or-group /openapi/em-runtime-governance-v2.yaml post /governance/permissions/revoke Removes a specific permission relation from a user or group on a resource. Used to revoke roles like owner, admin, developer, operator, or viewer. # Set resource parent relationship Source: https://docs.emergence.ai/api-reference/permissions/set-resource-parent-relationship /openapi/em-runtime-governance-v2.yaml post /governance/permissions/set-parent Establishes a parent-child relationship between resources for permission inheritance. For example, linking a project to an organization or a secret to a project. # Create a new project Source: https://docs.emergence.ai/api-reference/projects/create-a-new-project /openapi/em-runtime-governance-v2.yaml post /governance/projects Creates a new project within the user's organization (determined from JWT claims). The project is automatically linked to the organization and default project groups receive permissions. A UUID will be generated for the project ID unless external_id is provided. # Delete a project Source: https://docs.emergence.ai/api-reference/projects/delete-a-project /openapi/em-runtime-governance-v2.yaml delete /governance/projects/{project_id} Deletes a project and its project-scoped authorization tuples. # Get project details Source: https://docs.emergence.ai/api-reference/projects/get-project-details /openapi/em-runtime-governance-v2.yaml get /governance/projects/{project_id} Retrieves detailed information about a specific project by its ID. The user must have permission to view the project. # List projects Source: https://docs.emergence.ai/api-reference/projects/list-projects /openapi/em-runtime-governance-v2.yaml get /governance/projects List all projects in the user's organization with pagination support. # Root Source: https://docs.emergence.ai/api-reference/root/root /openapi/em-data-readiness-v2.yaml get /api/data-readiness/ # Sample Data Endpoint Source: https://docs.emergence.ai/api-reference/sample/sample-data-endpoint /openapi/em-talk2data-v2.yaml post /talk2data/v1/sample # Create a new schedule Source: https://docs.emergence.ai/api-reference/schedules/create-a-new-schedule /openapi/em-runtime-utils-v2.yaml post /utils/schedules Creates a new schedule for triggering workflows. The schedule can be either a cron-based recurring schedule or a one-time schedule. When triggered, notifications are published to the specified topic. # Delete a schedule Source: https://docs.emergence.ai/api-reference/schedules/delete-a-schedule /openapi/em-runtime-utils-v2.yaml delete /utils/schedules/{schedule_id} Permanently deletes a schedule. The schedule must belong to the authenticated user's organization. This action cannot be undone. # Delete a single schedule run Source: https://docs.emergence.ai/api-reference/schedules/delete-a-single-schedule-run /openapi/em-runtime-utils-v2.yaml delete /utils/schedules/runs/{run_id} Deletes a single schedule run by its ID. The run must belong to the authenticated user's organization. # Disable a schedule Source: https://docs.emergence.ai/api-reference/schedules/disable-a-schedule /openapi/em-runtime-utils-v2.yaml patch /utils/schedules/{schedule_id}/disable Disables a schedule and unschedules it from pg_cron. The schedule must belong to the authenticated user's organization. If the schedule is already disabled, this operation is idempotent. # Enable a schedule Source: https://docs.emergence.ai/api-reference/schedules/enable-a-schedule /openapi/em-runtime-utils-v2.yaml patch /utils/schedules/{schedule_id}/enable Enables a schedule and registers it with pg_cron. The schedule must belong to the authenticated user's organization. If the schedule is already enabled, this operation is idempotent. # Get a schedule Source: https://docs.emergence.ai/api-reference/schedules/get-a-schedule /openapi/em-runtime-utils-v2.yaml get /utils/schedules/{schedule_id} Retrieves a single schedule by its ID. The schedule must belong to the authenticated user's organization. # Get a single schedule run Source: https://docs.emergence.ai/api-reference/schedules/get-a-single-schedule-run /openapi/em-runtime-utils-v2.yaml get /utils/schedules/runs/{run_id} Retrieves a single schedule run by its ID, including the full schedule snapshot at execution time. The run must belong to the authenticated user's organization. # List schedule runs Source: https://docs.emergence.ai/api-reference/schedules/list-schedule-runs /openapi/em-runtime-utils-v2.yaml get /utils/schedules/runs Retrieves a paginated list of schedule runs for the authenticated user's organization. Results can be optionally filtered by project ID and ordered by creation time. # List schedules Source: https://docs.emergence.ai/api-reference/schedules/list-schedules /openapi/em-runtime-utils-v2.yaml get /utils/schedules Retrieves a list of all schedules associated with the authenticated user's organization. If a project ID is provided in the header, the results are filtered to only include schedules for that project. # Update a schedule Source: https://docs.emergence.ai/api-reference/schedules/update-a-schedule /openapi/em-runtime-utils-v2.yaml put /utils/schedules/{schedule_id} Updates an existing schedule. All fields in the request body are required - this is a full update. The schedule must belong to the authenticated user's organization. # Bulk index entities Source: https://docs.emergence.ai/api-reference/search/bulk-index-entities /openapi/em-runtime-search-v2.yaml post /search/bulk/index Add or update one or more entities in the global search index. Each item is validated then published to the internal event stream — embedding generation and indexing happen asynchronously. Returns 202 with the count of accepted operations. **Required fields**: ``organization_id``, ``project_id``, ``entity_type``, ``entity_id``, ``title``, ``permission_id``. **``embedded_text``**: The text used for BM25 indexing and embedding generation. For catalog entities this should be an LLM-generated contextual summary (~1000 tokens). For other entities use title + description concatenation. The embedding vector is generated asynchronously by the search handler — publishing services do not need to compute it. **``search_attributes``**: Flat ``dict[str, str]`` of fields that support structured query syntax on ``POST /search/global``. All values must be strings. Standard keys for catalog entities: ``name`` — entity name ``service_name`` — data source name (supports ``service.name:X``) ``database_name`` — database name (supports ``database.name:X``) ``schema_name`` — schema name (supports ``databaseSchema.name:X``) ``tags`` — space-separated tag FQNs (supports ``tags.tagFQN:PII*``) ``is_pii_tagged`` — 'true'/'false' (supports ``extension.is_pii_tagged:1``) ``is_sensitivity_tagged`` — 'true'/'false' ``has_description`` — 'true'/'false' (supports ``NOT _exists_:description``) ``has_owners`` — 'true'/'false' (supports ``NOT _exists_:owners``) ``has_undescribed_columns`` — 'true'/'false' (supports ``NOT _exists_:columns.description``) ``columns_tags_tagFQN`` — space-separated column tag FQNs (supports ``_exists_:columns.tags.tagFQN:PII*``) # Bulk remove entities from the index Source: https://docs.emergence.ai/api-reference/search/bulk-remove-entities-from-the-index /openapi/em-runtime-search-v2.yaml post /search/bulk/delete Remove one or more entities from the global search index. Each item is validated then published to the internal event stream — deletion happens asynchronously. Returns 202 with the count of accepted operations. # Global search Source: https://docs.emergence.ai/api-reference/search/global-search /openapi/em-runtime-search-v2.yaml post /search/global Unified search across all entity types — projects, assets, data connections, catalog entities (databases, schemas, tables, columns), files, artifacts, agents, and models. Results are filtered to entities the authenticated user can access. **Query modes** (set via the ``mode`` field): - ``semantic`` *(default)*: Plain natural language. ``q`` is run through BM25 and, when an embedding service is configured, also vectorised for hybrid RRF ranking. Best for conversational queries. - ``structured``: Elasticsearch-style query string in ``query``. Parsed by the Lark grammar and translated to SQL predicates against the ``search_attributes`` JSONB column and the ParadeDB BM25 index. Supports field:value, wildcards, boolean AND/OR/NOT, _exists_ checks, tag filtering, and extension field comparisons. Replaces the retired ``GET /search/metadata`` endpoint. - ``hybrid``: Both legs simultaneously. ``q`` drives semantic ranking; ``query`` applies structural constraints. Results are merged via Reciprocal Rank Fusion (RRF). **Pagination**: pass ``session_id`` from the first response on subsequent pages to reuse the scope session and avoid re-running the OpenFGA ListObjects fan-out. **Structured query syntax examples**: ``service.name:mysql AND tags.tagFQN:PII*`` — PII-tagged tables in a MySQL service ``NOT _exists_:owners AND entity_type:table`` — tables missing owners ``NOT _exists_:columns.description`` — tables with undescribed columns ``extension.is_pii_tagged:1 AND databaseSchema.name:public`` — PII tables in public schema ``(tags.tagFQN:SENSITIVITY_TAGS.Confidential OR tags.tagFQN:SENSITIVITY_TAGS.Restricted) AND service.name:snowflake`` — confidential or restricted Snowflake entities # Search metadata across resources Source: https://docs.emergence.ai/api-reference/search/search-metadata-across-resources /openapi/em-runtime-utils-v2.yaml get /utils/search/metadata Search catalog entities across all resources within a tenant, with filtering, sorting, and pagination. # Create a new organization secret Source: https://docs.emergence.ai/api-reference/secrets/create-a-new-organization-secret /openapi/em-runtime-governance-v2.yaml post /governance/resources/organizations/secrets Creates a new secret at the organization level. The secret is stored securely in Infisical and the creator is automatically granted owner permissions. The secret value should be provided as a JSON string matching the format required by the resource type. # Create a new project secret Source: https://docs.emergence.ai/api-reference/secrets/create-a-new-project-secret /openapi/em-runtime-governance-v2.yaml post /governance/resources/projects/secrets Creates a new secret at the project level. The secret is stored securely in Infisical and the creator is automatically granted owner permissions. The secret value should be provided as a JSON string matching the format required by the resource type. # Create a new secret Source: https://docs.emergence.ai/api-reference/secrets/create-a-new-secret /openapi/em-runtime-governance-v2.yaml post /governance/resources/{resource_type}/{resource_id}/secrets Creates a new secret within a specific resource. The secret is stored securely in Infisical and the creator is automatically granted owner permissions. The secret value should be provided as a JSON string matching the format required by the resource type. # Delete a project secret Source: https://docs.emergence.ai/api-reference/secrets/delete-a-project-secret /openapi/em-runtime-governance-v2.yaml delete /governance/resources/projects/secrets/{secret_name} Permanently deletes a project secret from Infisical and removes all associated permissions. Requires delete permissions on the secret. This action cannot be undone. # Delete a secret Source: https://docs.emergence.ai/api-reference/secrets/delete-a-secret /openapi/em-runtime-governance-v2.yaml delete /governance/resources/{resource_type}/{resource_id}/secrets/{secret_name} Permanently deletes a secret from Infisical and removes all associated permissions. Requires delete permissions on the secret. This action cannot be undone. # Delete an organization secret Source: https://docs.emergence.ai/api-reference/secrets/delete-an-organization-secret /openapi/em-runtime-governance-v2.yaml delete /governance/resources/organizations/secrets/{secret_name} Permanently deletes an organization secret from Infisical and removes all associated permissions. Requires delete permissions on the secret. This action cannot be undone. # Get organization secret value Source: https://docs.emergence.ai/api-reference/secrets/get-organization-secret-value /openapi/em-runtime-governance-v2.yaml get /governance/resources/organizations/secrets/{secret_name} Retrieves the actual secret value for a specific organization secret by name. This endpoint returns sensitive data and requires read permissions on the secret. The secret value is returned as stored in Infisical. # Get project secret value Source: https://docs.emergence.ai/api-reference/secrets/get-project-secret-value /openapi/em-runtime-governance-v2.yaml get /governance/resources/projects/secrets/{secret_name} Retrieves the actual secret value for a specific project secret by name. This endpoint returns sensitive data and requires read permissions on the secret. The secret value is returned as stored in Infisical. # Get secret value Source: https://docs.emergence.ai/api-reference/secrets/get-secret-value /openapi/em-runtime-governance-v2.yaml get /governance/resources/{resource_type}/{resource_id}/secrets/{secret_name} Retrieves the actual secret value for a specific secret by name. This endpoint returns sensitive data and requires read permissions on the secret. The secret value is returned as stored in Infisical. # List secrets for a project Source: https://docs.emergence.ai/api-reference/secrets/list-secrets-for-a-project /openapi/em-runtime-governance-v2.yaml get /governance/resources/projects/secrets Retrieves a list of all secrets associated with a specific project. Secrets are stored in Infisical and scoped by organization and project. Returns only secret metadata, not the actual secret values. # List secrets for a resource Source: https://docs.emergence.ai/api-reference/secrets/list-secrets-for-a-resource /openapi/em-runtime-governance-v2.yaml get /governance/resources/{resource_type}/{resource_id}/secrets Retrieves a list of all secrets associated with a specific resource. Secrets are stored in Infisical and scoped by organization, project, and resource. Returns only secret metadata, not the actual secret values. # List secrets for an organization Source: https://docs.emergence.ai/api-reference/secrets/list-secrets-for-an-organization /openapi/em-runtime-governance-v2.yaml get /governance/resources/organizations/secrets Retrieves a list of all secrets associated with the authenticated user's organization. Secrets are stored in Infisical and scoped by organization. Returns only secret metadata, not the actual secret values. # Update an existing organization secret Source: https://docs.emergence.ai/api-reference/secrets/update-an-existing-organization-secret /openapi/em-runtime-governance-v2.yaml put /governance/resources/organizations/secrets/{secret_name} Updates the value or description of an existing organization secret. Requires write permissions on the secret. The secret value and/or description can be updated independently. # Update an existing project secret Source: https://docs.emergence.ai/api-reference/secrets/update-an-existing-project-secret /openapi/em-runtime-governance-v2.yaml put /governance/resources/projects/secrets/{secret_name} Updates the value or description of an existing project secret. Requires write permissions on the secret. The secret value and/or description can be updated independently. # Update an existing secret Source: https://docs.emergence.ai/api-reference/secrets/update-an-existing-secret /openapi/em-runtime-governance-v2.yaml put /governance/resources/{resource_type}/{resource_id}/secrets/{secret_name} Updates the value or description of an existing secret. Requires write permissions on the secret. The secret value and/or description can be updated independently. # Service Info Source: https://docs.emergence.ai/api-reference/service-info /openapi/em-talk2data-v2.yaml get /talk2data # Service Info Source: https://docs.emergence.ai/api-reference/service-info-1 /openapi/em-talk2data-v2.yaml get /talk2data/ # Create a soft FK edge Source: https://docs.emergence.ai/api-reference/soft-fk-edges/create-a-soft-fk-edge /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/soft-fk-edges Create a new Soft FK edge within a context pack. # Expand seed tables via soft FK edges to find junction tables Source: https://docs.emergence.ai/api-reference/soft-fk-edges/expand-seed-tables-via-soft-fk-edges-to-find-junction-tables /openapi/em-runtime-utils-v2.yaml post /utils/context-packs/{name}/soft-fk-expand BFS expansion to find junction tables connecting seed tables. # Get a soft FK edge by ID Source: https://docs.emergence.ai/api-reference/soft-fk-edges/get-a-soft-fk-edge-by-id /openapi/em-runtime-utils-v2.yaml get /utils/context-packs/{name}/soft-fk-edges/{edge_id} Get an FK edge by ID. # List soft FK edges Source: https://docs.emergence.ai/api-reference/soft-fk-edges/list-soft-fk-edges /openapi/em-runtime-utils-v2.yaml get /utils/context-packs/{name}/soft-fk-edges List soft FK edges within a context pack. # Soft delete a soft FK edge Source: https://docs.emergence.ai/api-reference/soft-fk-edges/soft-delete-a-soft-fk-edge /openapi/em-runtime-utils-v2.yaml delete /utils/context-packs/{name}/soft-fk-edges/{edge_id} Soft delete a soft FK edge. # Update a soft FK edge Source: https://docs.emergence.ai/api-reference/soft-fk-edges/update-a-soft-fk-edge /openapi/em-runtime-utils-v2.yaml patch /utils/context-packs/{name}/soft-fk-edges/{edge_id} Update a soft FK edge's description, confidence, or status. # Get violation Source: https://docs.emergence.ai/api-reference/violations/get-violation /openapi/em-data-readiness-v2.yaml get /api/data-readiness/violations/{violation_id} Get a single violation snapshot by its ID. # List violation definitions Source: https://docs.emergence.ai/api-reference/violations/list-violation-definitions /openapi/em-data-readiness-v2.yaml get /api/data-readiness/violations/definitions List all available violation types and their configurations. # List violations Source: https://docs.emergence.ai/api-reference/violations/list-violations /openapi/em-data-readiness-v2.yaml get /api/data-readiness/violations List all violation snapshots with optional filtering and pagination. # Update violation Source: https://docs.emergence.ai/api-reference/violations/update-violation /openapi/em-data-readiness-v2.yaml patch /api/data-readiness/violations/{violation_id} Update a violation's status (open, resolved, false_positive, acknowledged). # Cancel All Active Runs Source: https://docs.emergence.ai/api-reference/workflows/cancel-all-active-runs /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows/{configuration_id}/runs/cancel Cancel all active (queued or running) runs for a configuration. # Cancel Workflow Run Source: https://docs.emergence.ai/api-reference/workflows/cancel-workflow-run /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows/{configuration_id}/runs/{run_id}/cancel Cancel a specific workflow run. # Create Pending Approval Source: https://docs.emergence.ai/api-reference/workflows/create-pending-approval /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows/runs/{run_id}/approval Create a pending approval record for a completed workflow run. This is typically called automatically when a workflow completes, but can also be called manually to create an approval for a run that doesn't have one. **Returns:** - 201: Approval created successfully - 404: Workflow run not found - 409: Approval already exists for this run # Create Workflow Configuration Source: https://docs.emergence.ai/api-reference/workflows/create-workflow-configuration /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows Create a new workflow configuration with custom settings, schedule, and triggers. This creates a configuration that can be used to run workflows with specific settings. Required: - `workflow_definition_id`: ID of the workflow definition to configure Optional: - `config`: Configuration overrides - `schedule`: Schedule configuration for automated execution - `triggers`: Event-based triggers for workflow execution # Create Workflow Definition Source: https://docs.emergence.ai/api-reference/workflows/create-workflow-definition /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows/definitions Create a new workflow definition (template). Workflow definitions are the deployed pipeline templates that can be configured with custom settings, schedules, and triggers. This endpoint is typically used by administrators to define new workflow types. Required: - `name`: Human-readable workflow name - `workflow_type`: Type of workflow (generate-pii-tags, generate-dq-tags, etc.) - `data_asset_types`: List of supported data asset types Optional: - `description`: Workflow description - `default_config`: Default configuration for this workflow type - `is_active`: Whether the workflow is active (default: true) # Create Workflow Run Source: https://docs.emergence.ai/api-reference/workflows/create-workflow-run /openapi/em-data-readiness-v2.yaml post /api/data-readiness/workflows/{configuration_id}/runs Trigger a new run of a workflow by configuration ID. This creates a workflow run record and triggers the associated Prefect deployment in fire-and-forget mode. The run status can be monitored via the get/list endpoints. Headers: - `Authorization`: Bearer token with JWT for authentication - `X-Project-ID`: Project resource URI (e.g., 'proj:customer-analytics') Optional: - `asset_fqn`: Fully qualified name of the data asset (e.g., 'PostgreSQL.dvdrental.public.customer') - `parameters`: Additional workflow execution parameters. For generate-table-description, use 'include' to specify a list of table FQNs to process. # Get Approval Status Source: https://docs.emergence.ai/api-reference/workflows/get-approval-status /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/runs/{run_id}/approval Get the approval status for a workflow run. **Returns:** - 200: Approval found - 404: Approval not found for this run # Get Pipeline Run Source: https://docs.emergence.ai/api-reference/workflows/get-pipeline-run /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/pipelines/{pipeline_id} Retrieve the status and progress counters for a run-all-enrichment pipeline run. # Get Task Source: https://docs.emergence.ai/api-reference/workflows/get-task /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id}/runs/{run_id}/tasks/{task_id} Get detailed information about a specific task. # Get Workflow Configuration Source: https://docs.emergence.ai/api-reference/workflows/get-workflow-configuration /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id} Retrieve detailed information about a specific workflow configuration. Returns the full configuration details including: - Configuration overrides - Schedule configuration - Trigger configurations - Workflow definition metadata (name, type, supported data asset types) The config, schedule, and triggers represent configuration-specific settings that feed into deployed pipelines without redeploying code. # Get Workflow Definition Source: https://docs.emergence.ai/api-reference/workflows/get-workflow-definition /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/definitions/{definition_id} Retrieve detailed information about a specific workflow definition. Workflow definitions are the deployed pipeline templates that can be configured with custom settings. This endpoint returns the full definition including supported data asset types and default configuration. # Get Workflow Run Source: https://docs.emergence.ai/api-reference/workflows/get-workflow-run /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id}/runs/{run_id} Retrieve detailed information about a specific workflow run, including all associated tasks. # Get Workflow Run Statistics Source: https://docs.emergence.ai/api-reference/workflows/get-workflow-run-statistics /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id}/runs/{run_id}/statistics Get statistics for a specific workflow run including task counts and duration. # Get Workflow Statistics Source: https://docs.emergence.ai/api-reference/workflows/get-workflow-statistics /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/statistics Get aggregated statistics for workflow runs. Optional filtering: - `configuration_id`: Filter statistics for a specific configuration. If not provided, returns statistics across all workflows. - `start_date`: Filter runs started after this date (ISO 8601) - `end_date`: Filter runs started before this date (ISO 8601) # List All Workflow Runs Source: https://docs.emergence.ai/api-reference/workflows/list-all-workflow-runs /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/runs Retrieve a paginated list of workflow runs across all workflows for the current tenant. Supports filtering by: - `asset_fqn`: Filter runs by asset FQN (prefix match) - `workflow_type`: Filter by workflow type - `status`: Comma-separated status list (e.g., 'running,failed,completed'). Valid statuses: pending, scheduled, running, completed, failed, cancelled, retrying, cached, crashed - `start_date`: Filter runs started after this date (ISO 8601) - `end_date`: Filter runs started before this date (ISO 8601) Supports sorting by: - `sort`: Multi-sort format: field:order,field:order (e.g., 'status:desc,started_at:asc'). Valid fields: workflow_name, started_at, status. Valid orders: asc, desc # List Tasks Source: https://docs.emergence.ai/api-reference/workflows/list-tasks /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id}/runs/{run_id}/tasks List tasks within a workflow run. # List Workflow Configurations Source: https://docs.emergence.ai/api-reference/workflows/list-workflow-configurations /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows Retrieve a paginated list of workflow configurations available to the current user. Supports filtering by: - `data_asset_type`: Filter configurations linked to a specific data asset type - `workflow_type`: Filter by type (generate-pii-tags, generate-dq-tags, etc.) - `status`: Filter by configuration status - `has_schedule`: Filter configurations with active schedules - `has_triggers`: Filter configurations with active triggers - `search`: Search by workflow name/description - `is_default`: Filter by default configuration status (pass `true` to list only the canonical default per workflow type) The response includes summary schedule information and triggers. Use the Get Configuration endpoint for full configuration details. # List Workflow Definitions Source: https://docs.emergence.ai/api-reference/workflows/list-workflow-definitions /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/definitions Retrieve a paginated list of workflow definitions (available workflow types). Workflow definitions are the deployed pipeline templates that can be configured with custom settings, schedules, and triggers. Supports filtering by: - `workflow_type`: Filter by workflow type - `is_active`: Filter by active status - `data_asset_type`: Filter workflows that support a specific data asset type This endpoint is useful for discovering available workflow types that can be configured for execution. # Stream Workflow Run Events (SSE) Source: https://docs.emergence.ai/api-reference/workflows/stream-workflow-run-events-sse /openapi/em-data-readiness-v2.yaml get /api/data-readiness/workflows/{configuration_id}/runs/{run_id}/events/stream Real-time event stream via Server-Sent Events. **Features:** - Streams all historical events first, then live events - Supports resumable connections via `Last-Event-ID` header - Clients reconnecting will catch up from the last seen sequence number **Usage:** 1. First connection: Receives all historical events, then live events 2. Reconnection: Send `Last-Event-ID: ` header to resume from that point # Update Workflow Approval Status Source: https://docs.emergence.ai/api-reference/workflows/update-workflow-approval-status /openapi/em-data-readiness-v2.yaml patch /api/data-readiness/workflows/runs/{run_id}/approval Update the approval status of a workflow run's output. **Request Body:** - `action`: One of "approve", "reject", or "pending" - `notes`: Optional notes about the decision **Approval Flow:** - When approved, the workflow output will be applied to the data asset - When rejected, the workflow output is discarded - When set to pending, the approval is reset for re-review (clears previous decision) **Returns:** - 200: Approval updated successfully - 400: Invalid action (e.g., trying to set to pending when already pending) - 404: Approval not found for this run # Data Enrichment Source: https://docs.emergence.ai/data-governance/data-enrichment How the Data Governance solution uses LLMs to enrich metadata with descriptions, classifications, and data quality rule suggestions. # Metadata Enrichment The metadata enrichment pipeline uses **LLMs** to automatically generate descriptions, classifications, and data quality rule suggestions for profiled data assets. This transforms raw schema information into rich, governed metadata that helps data teams understand and trust their data. ## What Gets Enriched | Enrichment | Description | | --------------------------- | ---------------------------------------------------------------------- | | **Business description** | Human-readable description of the table's purpose and contents | | **Domain classification** | Business domain the table belongs to (e.g., Sales, Finance, HR) | | **Sensitivity level** | Data classification level (Public, Internal, Confidential, Restricted) | | **Data steward suggestion** | Recommended owner based on domain and content | | **Relationship summary** | Description of how the table relates to other tables | | Enrichment | Description | | ------------------------ | --------------------------------------------------------------- | | **Business description** | What the column represents in business terms | | **Data type assessment** | Whether the declared type matches actual content | | **PII classification** | Whether the column contains personally identifiable information | | **Format pattern** | Expected format (email, phone, date, currency, etc.) | | **Valid value range** | Expected min/max or enumerated values | | **DQ rule suggestions** | Recommended data quality rules for this column | ## Enrichment Pipeline ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB API["Data Governance API\n(trigger)"] FLOW["Prefect Flow\n(enrich_metadata)"] CTR["ConcurrentTaskRunner\n(parallel tables)"] CTX["prepare_tables_and_schema()\n(full schema + profiling context)"] SEM["asyncio.Semaphore\n(LLM API rate gate)"] LLM["Vertex AI\n(gemini-3.5-flash)"] VAL["Validation\n(PII cross-check,\nschema alignment)"] DB[("datareadiness DB\n(enriched metadata)")] subgraph Outputs["Generated Metadata"] DESC["Business descriptions"] PII["PII classifications"] DQR["DQ rule suggestions"] end API --> FLOW --> CTR --> CTX --> SEM --> LLM --> VAL --> Outputs --> DB classDef trigger fill:#56B4E9,stroke:#555555,color:#000 classDef orch fill:#E69F00,stroke:#555555,color:#000 classDef gate fill:#F0E442,stroke:#555555,color:#000 classDef llm fill:#CC79A7,stroke:#555555,color:#000 classDef result fill:#009E73,stroke:#555555,color:#000 classDef store fill:#0072B2,stroke:#555555,color:#fff class API trigger class FLOW,CTR,CTX orch class SEM gate class LLM,VAL llm class DESC,PII,DQR result class DB store ``` The enrichment workflow is orchestrated by Prefect and uses the OpenAI API for LLM-powered analysis: The workflow gathers schema information and profiling results for the target tables. Schema context (all tables and relationships) is assembled using `prepare_tables_and_schema()` to give the LLM full database context. Schema and profiling data are sent to the LLM with structured prompts. The LLM generates metadata enrichments based on: * Column names and types * Profiling statistics (completeness, uniqueness, patterns) * Table relationships and foreign keys * Cross-table context (schema\_info for understanding relationships) Generated metadata is validated against the schema. PII classifications are cross-checked with column patterns and profiling data. Enriched metadata is stored in the Data Governance database, linked to the corresponding data asset records. ## Schema Context for Better Enrichment The enrichment pipeline provides full schema context to the LLM for higher-quality results: ```python theme={null} from dq.utils.schema_helpers import prepare_tables_and_schema # Gather schema context schema_info, tables_info = await prepare_tables_and_schema(connection) # schema_info: all tables in the database (for relationship understanding) # tables_info: selected tables for enrichment # Pass both to the LLM prompt prompt = build_enrichment_prompt( tables=tables_info, schema_context=schema_info # LLM understands cross-table relationships ) ``` Providing full schema context allows the LLM to understand foreign key relationships and generate more accurate business descriptions. For example, it can identify that `customer_id` in the `orders` table references the `customers` table and generate appropriate descriptions. ## Concurrency Control Enrichment workflows use the same hybrid concurrency model as profiling: | Level | Mechanism | Control | | -------------- | ---------------------- | --------------------------------------------- | | **Flow-level** | `ConcurrentTaskRunner` | Max concurrent table enrichment tasks | | **Task-level** | `asyncio.Semaphore` | Max concurrent LLM API calls within each task | The task-level semaphore is particularly important for enrichment to avoid hitting LLM API rate limits: ```python theme={null} @task async def enrich_table(table_info, schema_context): semaphore = asyncio.Semaphore(config.max_concurrent_llm_calls) columns = table_info["columns"] async def enrich_column(column): async with semaphore: return await llm_enrich(column, schema_context) results = await asyncio.gather(*[enrich_column(c) for c in columns]) return results ``` ## LLM Configuration The enrichment pipeline uses LiteLLM and is provider-agnostic — defaults to Vertex AI but works with any LiteLLM-supported provider: | Setting | Source | Description | | ---------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `VERTEXAI_PROJECT` / `VERTEXAI_LOCATION` | Environment variables | Vertex AI project and region (default `global`). LiteLLM also accepts the Google Cloud standards `GOOGLE_CLOUD_PROJECT` / `GOOGLE_CLOUD_LOCATION`. | | `GOOGLE_APPLICATION_CREDENTIALS` | Environment variable | Path to ADC JSON (local dev). Use Workload Identity in production. | | `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` | Environment variables | Only required if you swap the model to a non-Vertex provider | | Model | `config.yaml` | LiteLLM model identifier (e.g., `vertex_ai/gemini-3.5-flash`, `vertex_ai/claude-opus-4-8`, `openai/gpt-5.5`) | | Max tokens | `config.yaml` | Response token limit | | Temperature | `config.yaml` | Creativity level (low for factual metadata) | Enrichment prompts include database schema information and profiling statistics. Ensure your LLM provider's data handling policies are compatible with the sensitivity level of your data. For Restricted data, consider using self-hosted LLMs. ## DQ Rule Generation As part of enrichment, the LLM suggests data quality (DQ) rules for each column: | Rule Type | Example | | ---------------- | ------------------------------------------------ | | **Completeness** | "email column should be 100% non-null" | | **Format** | "phone column should match pattern XXX-XXX-XXXX" | | **Range** | "age column should be between 0 and 150" | | **Uniqueness** | "customer\_id should be unique" | | **Referential** | "product\_id should exist in products table" | | **Consistency** | "end\_date should be after start\_date" | Generated rules are stored as structured metadata and can be used by the DQ monitoring system to track quality over time. ## Enrichment Results Access enrichment results via the Data Governance API: ```json theme={null} { "table": "customers", "enriched_at": "2026-04-03T12:00:00Z", "business_description": "Customer master data containing contact information and account details for all registered customers.", "domain": "Sales", "sensitivity": "Confidential", "columns": [ { "name": "email", "business_description": "Primary email address for customer communication", "pii_classification": "PII", "suggested_rules": [ {"type": "completeness", "threshold": 1.0}, {"type": "format", "pattern": "email"} ] } ] } ``` ## Next Steps Profile your data before enrichment to provide statistical context. Learn about the Prefect orchestration system that runs enrichment. Understand PII classification in the context of GDPR. Review how enrichment outputs map to data classification levels. # Data Profiling Source: https://docs.emergence.ai/data-governance/data-profiling How the Data Governance solution profiles data assets at the column and table level to assess data quality, completeness, and statistical characteristics. # Data Profiling Data profiling is the foundation of the Data Governance solution. It analyzes connected databases to produce statistical profiles of tables and columns, identifying data quality issues, completeness gaps, and structural anomalies. ## What Data Profiling Captures For each column in a profiled table, the system computes: | Metric | Description | | -------------------------- | -------------------------------------------- | | **Completeness** | Percentage of non-null values | | **Uniqueness** | Ratio of distinct values to total rows | | **Data type distribution** | Actual types vs declared column type | | **Min / Max / Mean** | Statistical bounds for numeric columns | | **Standard deviation** | Spread of numeric values | | **Pattern analysis** | Common formats (email, phone, date patterns) | | **Top N values** | Most frequent values and their counts | | **Outlier detection** | Values beyond 2 standard deviations | For each table: | Metric | Description | | -------------------------- | ------------------------------- | | **Row count** | Total number of records | | **Column count** | Number of columns | | **Completeness score** | Average column completeness | | **Freshness** | Last update timestamp | | **Primary key analysis** | Key uniqueness and completeness | | **Foreign key validation** | Referential integrity checks | | **Duplicate detection** | Potential duplicate rows | ## Profiling Workflow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB API["Data Governance API\n(trigger / schedule)"] FLOW["Prefect Flow\n(profile_database)"] CTR["ConcurrentTaskRunner\n(max 10 tables parallel)"] SEM["asyncio.Semaphore\n(20 column analyses / table)"] SRC[("Source Database\n(via data connection)")] subgraph Results["Results"] COL["Column metrics\n(completeness, uniqueness,\npatterns, outliers)"] TBL["Table metrics\n(row counts, freshness,\nPK/FK validation)"] end DB[("datareadiness DB\n(results + timestamps)")] SC["Scorecard"] API --> FLOW --> CTR --> SEM --> SRC SEM --> COL & TBL COL & TBL --> DB --> SC classDef trigger fill:#56B4E9,stroke:#555555,color:#000 classDef orch fill:#E69F00,stroke:#555555,color:#000 classDef gate fill:#F0E442,stroke:#555555,color:#000 classDef src fill:#0072B2,stroke:#555555,color:#fff classDef result fill:#009E73,stroke:#555555,color:#000 class API trigger class FLOW,CTR orch class SEM gate class SRC src class COL,TBL,DB,SC result ``` Data profiling is orchestrated as a Prefect workflow: An administrator or data steward selects the tables and schemas to profile via the Data Governance API. The Prefect flow launches with `ConcurrentTaskRunner` for parallel table processing. Each table is profiled as an independent task. For each table, the workflow queries the database to compute column-level statistics. Queries are optimized using sampling for large tables. Profiling results are stored in the Data Governance database (`datareadiness`) with timestamps for historical tracking. A data quality scorecard summarizes the profiling results across all tables, highlighting issues that need attention. ## Concurrency Control Profiling workflows use hybrid concurrency to efficiently process large databases without overwhelming the source: ```python theme={null} @flow(task_runner=ConcurrentTaskRunner(max_workers=10)) async def profile_database(tables: list[str]): # Process up to 10 tables concurrently tasks = [profile_table(table) for table in tables] await asyncio.gather(*tasks) @task async def profile_table(table: str): semaphore = asyncio.Semaphore(20) # Within each table, run up to 20 column analyses concurrently async with semaphore: await analyze_column(column) ``` | Level | Mechanism | Configuration | | -------------- | ------------------------------------- | ------------- | | **Flow-level** | `ConcurrentTaskRunner(max_workers=N)` | `config.yaml` | | **Task-level** | `asyncio.Semaphore(N)` | `config.yaml` | Adjust the `max_workers` and semaphore limits based on your source database capacity. Start with conservative values and increase based on observed database load. ## Integration with Data Connections Profiling uses data connections registered in the platform's Assets service: 1. The data steward selects a registered data connection 2. The profiling workflow retrieves connection credentials from the Secrets API at runtime 3. A read-only database session is established for profiling queries 4. All profiling queries use the read-only user configured during [data source setup](/guides/data-source-setup) Profiling queries can be resource-intensive on large tables. Schedule profiling runs during off-peak hours or configure sampling thresholds for tables with millions of rows. ## Profiling Results Profiling results are accessible via the Data Governance API: ```bash theme={null} # Get profiling results for a table by FQN (e.g. analytics-db.public.customers) curl -X GET \ "https:///api/data-readiness/data-assets/profiles/tables/by-fqn?fqn=" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " ``` Results include: ```json theme={null} { "table": "customers", "profiled_at": "2026-04-03T10:00:00Z", "row_count": 150000, "columns": [ { "name": "email", "completeness": 0.98, "uniqueness": 0.97, "pattern": "email", "top_values": ["gmail.com", "outlook.com", "yahoo.com"] }, { "name": "phone", "completeness": 0.72, "uniqueness": 0.95, "pattern": "phone_us", "null_count": 42000 } ], "quality_score": 0.85 } ``` ## Scheduling Profiles Profiling can be scheduled for recurring execution via the platform's scheduling system: * **Daily profiles**: Track data quality trends over time * **Weekly profiles**: Standard cadence for most datasets * **On-demand**: Triggered manually for new data sources or after schema changes ## Next Steps Enrich metadata after profiling with LLM-powered descriptions. Learn about the Prefect workflow orchestration system. Connect a database to start profiling. Understand how profiling data is classified and protected. # CRAFT Data Governance Overview Source: https://docs.emergence.ai/data-governance/overview Automated data profiling, LLM-powered metadata enrichment, data quality rule generation, and scorecard reporting orchestrated by Prefect workflows. # Data Governance Overview Assess and improve the quality and completeness of your enterprise data assets with **Data Governance**. Profile data, enrich metadata using LLMs, generate data quality (DQ) rules, and produce scorecards, all orchestrated through automated workflows. This solution implements two of the [CRAFT modules](/getting-started/concepts#craft-modules): data profiling, coverage, and policy-compliance checks implement **CRAFT Assess**; metadata enrichment, DQ rule generation, and classification implement **CRAFT Enrich**. Data Governance uses **Prefect** for workflow orchestration with `ConcurrentTaskRunner` for flow-level parallelism and `asyncio.Semaphore` for task-level concurrency control. Workflows run on a Kubernetes work pool for production-grade scalability. ## Who Is It For Profile data assets, review LLM-generated metadata enrichment, and track data quality scores across your organization's data landscape. Generate data quality rules, review scorecards, and ensure metadata standards are met across the organization for regulatory compliance. Configure data connections, manage profiling workflows, and integrate data quality checks into your data pipelines. Discover and understand data assets through enriched metadata, quality scores, and automated profiling summaries. ## Key Capabilities Automatically analyze data assets to understand structure, distributions, patterns, and anomalies. Profiling produces statistical summaries that feed into enrichment and DQ rule generation. Use LLMs to automatically enrich metadata with business descriptions, data classifications, semantic types, and governance annotations. Schema-aware prompts ensure high-quality results. Automatically generate data quality rules based on profiling results and schema analysis. LLMs produce rules that are contextually relevant to the data's business meaning and relationships. Generate data quality scorecards that track metrics, violations, and trends across data assets. Scorecards provide executive-level visibility into data health. ### Service Architecture ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB subgraph API["Data Governance API (FastAPI)"] WF["Workflows module"] MT["Metrics module"] DA["Data Assets module"] VL["Violations module"] TM["Talk to Metadata module"] end subgraph Orch["Orchestration (Prefect)"] PP["profile_database"] PE["enrich_metadata"] PD["generate_dq_rules"] end subgraph Platform["Platform Services"] GOV["Governance\n(auth + authz)"] ASS["Assets\n(data connections)"] SEC["Secrets API"] end LLM["LLM (OpenAI)"] DB[("PostgreSQL\n(datareadiness)")] API --> Orch API --> Platform PE --> LLM PD --> LLM Orch --> DB API --> DB classDef api fill:#E69F00,stroke:#555555,color:#000 classDef orch fill:#56B4E9,stroke:#555555,color:#000 classDef plat fill:#0072B2,stroke:#555555,color:#fff classDef ext fill:#CC79A7,stroke:#555555,color:#000 classDef db fill:#009E73,stroke:#555555,color:#000 class WF,MT,DA,VL,TM api class PP,PE,PD orch class GOV,ASS,SEC plat class LLM ext class DB db ``` | API Module | Purpose | | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **Workflows** | Trigger and monitor workflow runs | | **Metrics** | Track data quality metrics | | **Data Assets** | Manage registered data assets; batch profile lookups via `POST /profiles/columns/batch` and `POST /profiles/tables/batch` | | **Violations** | Track DQ rule violations | | **Talk to Metadata** | LLM-powered metadata interaction | Each domain module follows a consistent structure: * `models/requests/` -- Pydantic request models * `models/responses/` -- Pydantic response models * `services/` -- Business logic * `routes.py` -- FastAPI router * `dependencies.py` -- Dependency injection ## Workflow Orchestration Prefect orchestrates all data processing workflows with a hybrid concurrency model: Flows use `ConcurrentTaskRunner` to run multiple table-level tasks in parallel. The concurrency limit is configurable via `config.yaml` (e.g., `DQ_RULE_GEN_MAX_CONCURRENT_TABLES`). This provides: * Parallel processing across tables * Visibility in the Prefect UI * Configurable concurrency limits Within each Prefect task, `asyncio.Semaphore` controls fine-grained concurrency for operations like LLM API calls. Limits are configurable via `config.yaml` (e.g., `DQ_RULE_GEN_MAX_CONCURRENT_RULES`). This prevents: * API rate limit exhaustion * Memory pressure from too many concurrent operations * Unpredictable costs from uncontrolled LLM calls Production workflows run on a Kubernetes work pool named `kubernetes-pool`. Flow definitions are managed as code in `scripts/prefect/deploy.py` (single source of truth). Key details: * Work pool is auto-created during setup * Docker images are tagged with git commit hashes for traceability * Secrets are injected via Helm `secretKeyRef` into pod environment variables Workflow and infrastructure errors are wrapped in a `WorkflowRuntimeError` before being returned to API consumers. This normalizes raw Prefect and infrastructure stack traces into structured error responses, making errors actionable without exposing internal implementation details. ## DQ Rule Generation DQ rule generation combines profiling results with LLM intelligence: The system loads the complete schema context (`prepare_tables_and_schema()`) including all tables, columns, foreign keys, and relationships. Users can select specific tables for rule generation, or process the entire database. When `selected_tables` is specified in event-triggered ingestion, only those tables are processed — the filter is enforced server-side. Schema context and profiling statistics are sent to the LLM, which generates data quality rules that understand business semantics and cross-table relationships. Generated rules are validated against the schema and persisted to the database. Rules include dimension (completeness, accuracy, consistency, etc.) and severity levels. ## Database Architecture | Aspect | Details | | ---------------- | ------------------------------------------------------------------------------------------------ | | **ORM** | SQLAlchemy 2.0+ with async support | | **Database** | PostgreSQL 18+ (`datareadiness` database) | | **Migrations** | Alembic with clean two-migration approach: initial schema + seed data | | **Primary keys** | UUID v4 using PostgreSQL `gen_random_uuid()` | | **Timestamps** | `TIMESTAMPTZ` (TIMESTAMP WITH TIME ZONE) | | **Seed data** | 112 records: 11 workflows + 11 configs + 12 dimensions + 16 metrics + 37 violations + 25 DQ tags | The project uses a **clean two-migration approach**: one migration for the complete schema (tables, indexes, constraints) and a second for seed data. Seed data uses idempotent `INSERT ... ON CONFLICT DO NOTHING` for safe re-runs. ## Configuration Data Governance uses **OmegaConf** for hierarchical configuration: | Source | Priority | Example | | --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Environment variables | Highest | `VERTEXAI_PROJECT` / `VERTEXAI_LOCATION` (LLM, CRAFT default), `REDIS_PASSWORD`; `OPENAI_API_KEY` only if a non-Vertex provider is configured | | `config.yaml` | Default | Database URLs, concurrency limits, workflow parameters | | Internal references | -- | `${.field_name}` for self-referencing config values | Configuration is loaded once at module import via `common/config.py`. Environment variables can be referenced in YAML using `${oc.env:VAR_NAME, default}` syntax. ## Supported Data Sources Data Governance connects to external data sources for profiling, enrichment, and DQ rule generation. Supported connector types: | Connector | Auth | Notes | | -------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **PostgreSQL** | Username + password | Native async (`asyncpg`) | | **Redshift** | Username + password | Via `redshift_connector` | | **Snowflake** | RSA key-pair only | `asyncio.to_thread()` wrapping (no native async driver); account identifier format (e.g., `ORGNAME-ACCOUNTNAME`); service users must be `TYPE=SERVICE` with a mounted `.p8` private key | Connector selection is determined by the data connection type registered in the Assets service. Credentials are injected via Kubernetes secrets at runtime — never stored in the database. ## Platform Integration Data Governance integrates with the em-runtime platform for shared services: | Capability | Platform Service | Integration | | -------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | **Authentication** | Governance (8000) | JWT validation via Keycloak | | **Authorization** | Governance (8000) | Permission checks via OpenFGA SDK | | **Data connections** | Assets (8000) | Registered data source configurations (PostgreSQL, Redshift, Snowflake) | | **Secrets** | Infisical or ESO + GCP Secret Manager, see [Secrets Management](/security/secrets-management) | Credentials for data connections and API keys | ## Tech Stack | Technology | Purpose | | --------------- | ------------------------------------------------ | | Python 3.12+ | Service and workflow implementation | | FastAPI | Async REST API framework | | Prefect | Workflow orchestration with Kubernetes work pool | | PostgreSQL 18+ | Primary data store | | Redis 7+ | Caching and messaging | | SQLAlchemy 2.0+ | Async ORM with `asyncpg` | | OmegaConf | Hierarchical YAML configuration | | OpenAI API | LLM-powered enrichment and rule generation | | Docker Compose | Local development (PostgreSQL, Redis, Prefect) | | uv | Python workspace monorepo management | | pytest + memray | Testing with memory profiling | ## Next Steps Learn how automated profiling analyzes your data assets. Understand LLM-powered metadata enrichment workflows. Configure and monitor Prefect-orchestrated data governance workflows. Connect your data sources to the platform. # Workflows Source: https://docs.emergence.ai/data-governance/workflows How the Data Governance solution uses Prefect for workflow orchestration, including profiling, enrichment, and DQ rule generation workflows. # Workflow Orchestration The Data Governance solution uses **Prefect** for orchestrating data profiling, metadata enrichment, and data quality (DQ) rule generation workflows. Prefect provides task scheduling, concurrency control, observability, and failure handling. ## Architecture | Component | Purpose | | ------------------ | -------------------------------------------- | | **Prefect Server** | Workflow API and UI (port 4200) | | **Prefect Worker** | Executes flows in Kubernetes pods | | **Work Pool** | Kubernetes work pool named `kubernetes-pool` | | **PostgreSQL** | Prefect metadata storage | All Prefect services are deployed alongside the Data Governance API via Docker Compose (local dev) or Helm (Kubernetes). Flow definitions are managed as code in `scripts/prefect/deploy.py` -- the single source of truth for all deployments. This file registers flows, configures schedules, and sets job variables. ```python theme={null} # scripts/prefect/deploy.py (simplified) from prefect import flow from prefect.deployments import Deployment deployments = [ Deployment.build_from_flow( flow=profile_database, name="profile-database", work_pool_name="kubernetes-pool", job_variables={...} ), Deployment.build_from_flow( flow=enrich_metadata, name="enrich-metadata", work_pool_name="kubernetes-pool", job_variables={...} ), ] ``` `prefect.yaml` is deprecated. All flow definitions are managed in `deploy.py`. Secrets are read from pod environment variables (injected via Helm secretKeyRef) and passed as plain strings to Prefect job\_variables. ## Available Workflows ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR TRG["API / Schedule\nTrigger"] subgraph WF["Prefect Workflows (ConcurrentTaskRunner)"] PRF["profile_database\n(Profiling)"] ENR["enrich_metadata\n(Enrichment)"] DQG["generate_dq_rules\n(DQ Generation)"] end SEM["asyncio.Semaphore\n(task-level rate gate)"] LLM["LLM API\n(OpenAI)"] DB[("PostgreSQL\n(datareadiness)")] APR{"User\nApproval"} SC["Scorecard\nUpdate"] TRG --> PRF & ENR & DQG PRF & DQG --> SEM ENR --> SEM SEM --> LLM PRF & ENR & DQG --> DB DB --> APR APR -->|"approved"| SC classDef trigger fill:#56B4E9,stroke:#555555,color:#000 classDef wf fill:#E69F00,stroke:#555555,color:#000 classDef gate fill:#F0E442,stroke:#555555,color:#000 classDef ext fill:#CC79A7,stroke:#555555,color:#000 classDef store fill:#009E73,stroke:#555555,color:#000 classDef decision fill:#F0E442,stroke:#555555,color:#000 classDef terminal fill:#009E73,stroke:#555555,color:#000 class TRG trigger class PRF,ENR,DQG wf class SEM gate class LLM ext class DB store class APR decision class SC terminal ``` ### Data Profiling Flow Analyzes connected data sources to produce statistical profiles: | Setting | Value | | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Flow name** | `profile-database` | | **Concurrency** | `ConcurrentTaskRunner(max_workers=10)` | | **Input** | Data connection ID, schema/table targets (database-level targets are not supported, specify schema or table), sampling config | | **Output** | Column and table-level profiling results | Profiling, Metadata Assessment, and DQ Assessment workflows require **schema-level or table-level targets**. Database-level targets (entire database) are not supported, specify individual schemas or tables to process. ### Metadata Enrichment Flow Uses LLMs to generate business descriptions and classifications: | Setting | Value | | --------------- | --------------------------------------------------------------- | | **Flow name** | `enrich-metadata` | | **Concurrency** | `ConcurrentTaskRunner(max_workers=5)` | | **Input** | Data connection ID, profiling results | | **Output** | Business descriptions, PII classifications, DQ rule suggestions | ### DQ Rule Generation Flow Generates data quality rules based on profiling data and LLM analysis: | Setting | Value | | --------------- | ----------------------------------------------------------------------- | | **Flow name** | `generate-dq-rules` | | **Concurrency** | `ConcurrentTaskRunner` with `asyncio.Semaphore` for LLM calls | | **Input** | Schema context, profiling results, enrichment metadata | | **Output** | Structured DQ rules per column | | **Config** | `DQ_RULE_GEN_MAX_CONCURRENT_TABLES`, `DQ_RULE_GEN_MAX_CONCURRENT_RULES` | DQ rule generation does **not** require a reference pack. Earlier versions passed an industry context / reference pack into the workflow; that input was removed because it caused crashes under Prefect 3.x. Generation runs purely from schema context, profiling results, and metadata. ## Workflow approval and the scorecard When you approve ("Save") a profiling or enrichment workflow run, the platform applies two changes atomically: 1. **Tombstone affected metric snapshots.** Affected `metric_snapshots` rows are written with `numerator=0, denominator=0`. This invalidates the prior score so the scorecard does not display stale numbers while the next assessment is pending. 2. **Resolve all matching OPEN violations.** Every `OPEN` violation tied to an invalidated metric is closed in the same transaction, not just the most recent. Previously the resolve step closed only the latest violation, leaving older OPEN duplicates on the dashboard. Net effect for users: scorecards update in real time. After Save, expect to see the affected scorecard cells flip to **N/A** ("pending next assessment") and any related violation entries disappear from the open-violations view. ### FQN scoping for violations Violations are filtered by `asset_fqn` using an exact-or-child boundary: a filter for `db.schema.card` matches `db.schema.card` itself and any column-level child like `db.schema.card.amount`, but does **not** match sibling tables with prefix collisions like `db.schema.cardtransaction`. Earlier versions used a plain `startswith`, which produced false matches on sibling tables sharing a name prefix. ### Ingestion triggered by `asset.created` When an ingestion run is triggered by an `asset.created` event (or kicked off manually with no schemas listed in `config`), the workflow ingests **all schemas** in the connection, scoped by `config.database`. Earlier versions only ingested the first schema returned by the connection, which led to silent data gaps when a connection exposed multiple schemas. ## Concurrency Model Workflows use a **hybrid concurrency** model combining Prefect's task runner with asyncio semaphores: ### Flow-Level Concurrency The `ConcurrentTaskRunner` controls how many table-level tasks run simultaneously: ```python theme={null} @flow(task_runner=ConcurrentTaskRunner(max_workers=10)) async def profile_database(tables: list[str]): # Up to 10 tables profiled concurrently tasks = [profile_table.submit(table) for table in tables] results = await asyncio.gather(*[t.result() for t in tasks]) return results ``` ### Task-Level Concurrency Within each task, `asyncio.Semaphore` controls fine-grained concurrency (e.g., LLM API calls): ```python theme={null} @task async def enrich_table(table_info): semaphore = asyncio.Semaphore(20) # Max 20 concurrent LLM calls async def process_column(column): async with semaphore: return await llm_client.enrich(column) results = await asyncio.gather( *[process_column(col) for col in table_info.columns] ) return results ``` | Level | Mechanism | Visibility | | ----- | ---------------------- | --------------------- | | Flow | `ConcurrentTaskRunner` | Visible in Prefect UI | | Task | `asyncio.Semaphore` | Internal to the task | Configure concurrency limits via `config.yaml` rather than hardcoding. This allows tuning based on the source database capacity and LLM API rate limits. ## Workflow Management ### Prefect UI The Prefect UI (port 4200) provides visibility into workflow execution: * **Flow runs**: Status, duration, logs for each execution * **Task runs**: Individual task status within a flow * **Deployments**: Registered flow definitions and schedules * **Work pool**: Worker status and queue depth ## Configuration Workflow configuration is managed via **OmegaConf** with environment variable overrides: ```yaml theme={null} # config.yaml profiling: max_concurrent_tables: 10 sample_threshold: 1000000 # Sample tables with more than 1M rows sample_size: 100000 enrichment: max_concurrent_tables: 5 max_concurrent_llm_calls: 20 model: "vertex_ai/gemini-3.5-flash" temperature: 0.1 dq_rules: max_concurrent_tables: 10 max_concurrent_rules: 20 ``` **Precedence:** Environment variables > `config.yaml` > defaults Environment variables can be referenced in config via OmegaConf's `${oc.env:VAR_NAME, default}` syntax. The CRAFT default is Vertex AI — common env vars are `${oc.env:VERTEXAI_PROJECT}` / `${oc.env:VERTEXAI_LOCATION, global}` (also accepts the Google Cloud SDK standards `GOOGLE_CLOUD_PROJECT` / `GOOGLE_CLOUD_LOCATION`) with `GOOGLE_APPLICATION_CREDENTIALS` for ADC. Provider-specific keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) are only required when the configured model routes to a non-Vertex provider. ## Error Handling | Error Type | Handling | | --------------------------- | ----------------------------------------------------------- | | Database connection failure | Flow fails fast with descriptive error | | LLM API rate limit | Retry with exponential backoff via tenacity | | LLM API timeout | Retry up to 3 times, then skip column and continue | | Single table failure | Log error, continue processing remaining tables | | Infrastructure failure | Prefect marks flow as failed; manual retry available via UI | ## Kubernetes Deployment In Kubernetes, Prefect workflows run as pods in the `kubernetes-pool` work pool: | Component | Deployment | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Prefect Server | StatefulSet with PostgreSQL backend | | Prefect Worker | Deployment watching the kubernetes-pool | | Flow runs | Ephemeral pods created per flow execution | | Secrets | Injected via Helm `secretKeyRef`. For Vertex AI (CRAFT default): mount the ADC JSON via `GOOGLE_APPLICATION_CREDENTIALS` or use Workload Identity. For non-Vertex providers: `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` per provider. | ## Next Steps Learn about the profiling workflow in detail. Understand the LLM-powered enrichment pipeline. Connect a database to use with governance workflows. # Analysis Agent Source: https://docs.emergence.ai/data-insights/analysis-agent How the Insights Agent performs multi-step analysis with LLM-generated Python code, reasoning loops, and tool orchestration. # Analysis Agent The **Insights Agent** is the reasoning engine at the center of the Data Insights solution. It receives natural-language questions, determines the optimal analysis approach, and orchestrates tools and sub-agents to produce comprehensive answers. ## Role in the Architecture The Insights Agent sits between the Talk2Data Service (user-facing gateway) and specialized agents like Text2SQL: ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD T2D["Talk2Data Service (8080)
user-facing gateway"] IA["Insights Agent (8002)
reasoning engine (agentic loop)"] SQL["Text2SQL Agent (8001)
SQL generation + execution"] CODE["Coding Agent (8004)
Python code generation"] MCP["MCP Plotly Server (8000)
visualization tools"] T2D -->|A2A| IA IA -->|A2A| SQL IA -->|A2A| CODE IA -->|MCP| MCP classDef gateway fill:#56B4E9,stroke:#555555,color:#000 classDef agent fill:#E69F00,stroke:#555555,color:#000 classDef tool fill:#009E73,stroke:#555555,color:#000 class T2D gateway class IA,SQL,CODE agent class MCP tool ``` It acts as an **agentic loop** -- iteratively reasoning about the question, calling tools, analyzing intermediate results, and deciding whether to continue analysis or return a final answer. ## Capabilities For questions that can be answered with database queries, the Insights Agent delegates to the Text2SQL Agent: * "What were the top 10 products by revenue last quarter?" * "How many active users do we have per region?" * "Show me the trend of order values over the past 12 months" The Insights Agent adds value by interpreting the results, identifying patterns, and generating follow-up analysis suggestions. For complex analytical questions, the Insights Agent generates and executes Python code: * Statistical analysis (correlations, distributions, outlier detection) * Data transformations and pivots * Time series forecasting * Custom calculations not expressible in SQL ```python theme={null} # Example: LLM-generated Python for correlation analysis import pandas as pd import numpy as np df = pd.DataFrame(query_results) correlation = df[['revenue', 'marketing_spend']].corr() outliers = df[df['revenue'] > df['revenue'].mean() + 2 * df['revenue'].std()] ``` For complex questions requiring multiple data gathering and analysis steps: 1. Query database for raw data 2. Analyze initial results 3. Generate follow-up queries based on findings 4. Synthesize a comprehensive answer 5. Generate visualizations for key findings ## Agentic Loop The Insights Agent operates as an iterative reasoning loop: The agent receives the user's question along with conversation history (for multi-turn context) and the data connection schema. The LLM determines the analysis strategy: direct SQL query, multi-step analysis, Python computation, or visualization. The agent calls the appropriate tool or sub-agent. Results are captured and fed back into the reasoning loop. The LLM evaluates whether the results answer the question completely. If not, it plans additional steps. Once the analysis is complete, the agent formats the final response with text, data, and optional visualizations. ## Tool Orchestration The Insights Agent has access to multiple tools: | Tool | Purpose | Invocation | | --------------------- | --------------------------------------------------------------------------- | ------------------------------ | | **Text2SQL Agent** | Generate and execute SQL queries | A2A delegation (port 8001) | | **Coding Agent** | Run LLM-generated Python code for statistics, transformations, and analysis | A2A delegation (port 8004) | | **MCP Plotly Server** | Generate interactive Plotly charts | MCP tool call (port 8000) | | **Schema inspector** | Retrieve database schema details | Direct database metadata query | ## Event Streaming Throughout the analysis, the Insights Agent emits real-time events via the A2A protocol: ```python theme={null} # Status events keep the user informed of progress await event_queue.put(TaskStatusUpdateEvent( status="working", message="Analyzing your data..." )) # Artifact events deliver results await event_queue.put(TaskArtifactUpdateEvent( artifact=Artifact( parts=[DataPart(data=results), TextPart(text=analysis)] ) )) ``` Users see progress messages in the chat interface: "Analyzing schema...", "Running query...", "Computing statistics...", followed by the final answer. ## LLM Configuration The Insights Agent uses LiteLLM for provider-agnostic LLM access: | Setting | Configuration | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Client** | `commons.llm.LLMClient` wrapping LiteLLM | | **Model format** | `provider/model` (e.g., `vertex_ai/gemini-3.5-flash`, `vertex_ai/claude-opus-4-8`, `openai/gpt-5.5`) | | **Observability** | Langfuse LLM tracing auto-enabled when `LANGFUSE_HOST` is set, traces LLM calls, token usage, and cost. See [Langfuse Setup](/guides/langfuse-setup) for configuration. | | **Configuration** | `pydantic_settings.BaseSettings` with `.env` file support | ## Error Handling The Insights Agent handles errors at each stage of the analysis: | Error | Handling | | --------------------------- | ----------------------------------------------------------- | | Text2SQL fails | Retry with rephrased prompt or fall back to Python analysis | | Python execution error | Return error details to LLM for correction and retry | | LLM timeout or rate limit | Retry with exponential backoff via tenacity | | Data connection unavailable | Report connection error to the user | ## Conversation State The agent maintains state via the Talk2Data Service's session system: * **Sessions**: Persistent conversation containers linked to a data connection * **Messages**: Full conversation history with user and assistant messages * **Artifacts**: Query results, visualizations, and analysis outputs stored per session * **Feedback**: User feedback on answer quality for continuous improvement ## Next Steps See the full conversational analytics pipeline. Deep dive into the SQL generation agent. Learn how the Insights Agent generates charts. # Chat With Data Source: https://docs.emergence.ai/data-insights/chat-with-data How the CRAFT conversational data analytics pipeline works, from natural-language questions to SQL-generated answers with multi-turn context. # Chat With Data CRAFT's Data Insights solution enables users to ask natural-language questions against their databases and receive instant answers, visualizations, and analytical insights. The system uses a multi-agent architecture built on Google's A2A protocol to orchestrate the full pipeline from question to answer. ## How It Works When a user asks a question, the platform orchestrates a pipeline across three agents: The user types a natural-language question in the chat interface (e.g., "What were the top 10 products by revenue last quarter?"). The Talk2Data Service receives the request and creates a session context. The Insights Agent receives the question and determines the best approach. It may: * Route directly to the Text2SQL Agent for database queries * Execute a multi-step analysis plan for complex questions * Generate Python code for statistical analysis * Produce visualizations for data-oriented answers The Text2SQL Agent converts the natural-language question into SQL using schema-aware generation. It validates the SQL with `sqlglot`, executes it against the connected database, and returns the results. Results flow back through the agent chain via A2A events. The user sees real-time progress messages ("Analyzing schema...", "Generating SQL...", "Executing query...") followed by the final answer with data tables and optional visualizations. ## Architecture The Data Insights system consists of three services communicating via the A2A protocol: | Service | Port | Role | | --------------------- | ---- | ------------------------------------------------------------------------------------- | | **Talk2Data Service** | 8080 | FastAPI REST + SSE gateway. Manages sessions, chat history, and artifact storage | | **Insights Agent** | 8002 | Main agentic loop. Reasoning engine that orchestrates tool calls and agent delegation | | **Text2SQL Agent** | 8001 | Specialized agent for NL-to-SQL generation, validation, and execution | ### Communication Flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD Client["Browser / API Client"] T2D["Talk2Data Service (8080)"] IA["Insights Agent (8002)"] SQL["Text2SQL Agent (8001)"] DB[("Customer Database")] Client -->|"REST + SSE"| T2D T2D -->|"A2A (JSON-RPC 2.0 / SSE)"| IA IA -->|"A2A (JSON-RPC 2.0 / SSE)"| SQL SQL -->|"SQL via data connection"| DB classDef client fill:#56B4E9,stroke:#555555,color:#000 classDef svc fill:#E69F00,stroke:#555555,color:#000 classDef data fill:#009E73,stroke:#555555,color:#000 class Client client class T2D,IA,SQL svc class DB data ``` ## Multi-Turn Conversations The system maintains conversation context across multiple turns using the A2A **Context ID** (mapped to the session ID): * Each session has a conversation history stored in PostgreSQL * Previous questions and answers are included in the LLM context window * The Insights Agent can reference earlier results ("Now show me the same data as a pie chart") * Session artifacts (query results, visualizations) persist for the duration of the session * **Session context items** (`/sessions/{id}/context`) allow storing key-value metadata per session (e.g., selected data connection, user preferences) Multi-turn context allows follow-up questions like "What about last year?" or "Break that down by region" without repeating the full original question. ## A2A Protocol Integration All inter-agent communication uses Google's A2A protocol: | Concept | Usage | | ----------------- | ---------------------------------------------------------------------------------------------- | | **Agent Cards** | Each agent exposes `/.well-known/agent-card.json` describing capabilities and skills | | **Message Parts** | `TextPart` for questions/answers, `DataPart` for datasource metadata, `FilePart` for artifacts | | **Events** | `TaskStatusUpdateEvent` for progress, `TaskArtifactUpdateEvent` for results | | **Streaming** | Server-Sent Events (SSE) for real-time updates to the frontend | ## LLM Integration The Data Insights agents use **LiteLLM** for provider-agnostic LLM access: ```python theme={null} from commons.llm import LLMClient client = LLMClient() # Model format: "provider/model" response = await client.complete( model="vertex_ai/gemini-3.5-flash", messages=[{"role": "user", "content": question}] ) ``` Supported providers include Gemini, GPT, Claude, and any LiteLLM-compatible model. LLM observability is provided by **Langfuse** when `LANGFUSE_HOST` is configured, traces every LLM call with token counts, cost, and latency. See [Langfuse Setup](/guides/langfuse-setup) for observability configuration. ## Data Connection Requirements Chat with Data requires a registered data connection in the platform: 1. Register a PostgreSQL database via the Assets API (see [Data Source Setup](/guides/data-source-setup)) 2. Create a session linked to the data connection ID 3. The Text2SQL Agent fetches the database schema and uses it for context-aware SQL generation ## Next Steps Deep dive into the NL-to-SQL generation pipeline. Learn about the Insights Agent's reasoning and analysis capabilities. Understand how charts and visualizations are generated. Connect a database to start chatting with your data. # CRAFT Data Insights Overview Source: https://docs.emergence.ai/data-insights/overview Multi-agent conversational data analytics using the A2A protocol for natural-language to SQL generation, analysis, and visualization. # Data Insights Overview Ask natural-language questions about your data and get instant SQL-generated answers, visualizations, and insights. **Data Insights** coordinates a team of AI agents that generate SQL, execute queries, analyze results, and produce visualizations -- all in real time via streaming. Data Insights is built on the **A2A (Agent-to-Agent) protocol**, an open standard for inter-agent communication using JSON-RPC 2.0 over Server-Sent Events (SSE). Each agent is independently deployable and discoverable via Agent Cards. ## Who Is It For Ask questions about your data in plain language and get instant answers, visualizations, and reports -- no SQL or technical skills required. Explore data through multi-turn conversations, generate visualizations, and export results. The AI handles SQL generation so you can focus on analysis. Get AI-powered insights from enterprise data to support strategic decisions. Generate on-demand reports and discover trends through natural-language queries. Validate SQL generation, review query plans, and configure data connections. Monitor agent performance and query execution through observability tooling. ## How It Works ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff', 'actorBkg': '#56B4E9', 'actorBorder': '#555555', 'actorTextColor': '#000000', 'actorLineColor': '#555555', 'signalColor': '#555555', 'signalTextColor': '#555555', 'noteBkgColor': '#F0E442', 'noteBorderColor': '#555555', 'noteTextColor': '#000000', 'activationBkgColor': '#E69F00', 'activationBorderColor': '#555555'}}}%% sequenceDiagram participant U as User participant SVC as Talk2Data Service participant IA as Insights Agent participant SQL as Text2SQL Agent participant PG as PostgreSQL U->>SVC: Natural-language question SVC->>IA: Route message (A2A) IA->>SQL: Delegate SQL generation (A2A) SQL->>SQL: Analyze schema + generate SQL SQL->>PG: Execute validated query PG-->>SQL: Result set SQL-->>IA: Query results (A2A artifact) IA->>IA: Analyze results, generate insights IA-->>SVC: Insights + visualizations (SSE stream) SVC-->>U: Real-time streamed response ``` A user types a natural-language question in the chat interface, such as "What were the top 10 products by revenue last quarter?" The Talk2Data Service (REST + SSE gateway) creates a session, routes the message to the Insights Agent, and establishes an SSE stream back to the client. The Insights Agent runs an agentic loop: it reasons about the question, selects tools, and delegates SQL generation to the Text2SQL Agent. The Text2SQL Agent analyzes the database schema, generates SQL using an LLM, validates it with `sqlglot`, and executes the query against the user's data connection. Query results, analysis, and visualizations stream back to the user in real time via A2A events (`TaskStatusUpdateEvent` for progress, `TaskArtifactUpdateEvent` for results). ## Architecture Building a similar solution? Use the [Solution Developer Guide › Starter Templates](/guides/solution-dev/starter-templates) — the Tier 3 template mirrors this layered agent shape with copy-pasteable scaffolds. The solution follows a layered agent architecture: ```text theme={null} Client / React UI | Talk2Data Service (REST + SSE gateway, port 8080) | Insights Agent (agentic loop, reasoning, tool orchestration, port 8002) |--- Text2SQL Agent (NL-to-SQL generation, validation, execution, port 8001) |--- Coding Agent (LLM-generated Python code execution, port 8004) |--- MCP Plotly Server (visualization tools via MCP, port 8000) ``` Text2SQL and Coding agents communicate with the Insights Agent via A2A, while the MCP Plotly Server is accessed via MCP. The Insights Agent dynamically discovers both sub-agents and MCP tools on each request. ## A2A Protocol Integration All inter-agent communication uses the A2A protocol: Each agent publishes a JSON manifest at `/.well-known/agent-card.json` that describes its identity, skills, and capabilities. Agent Cards enable automatic discovery by the Talk2Data service and Insights Agent at request time. Key fields include: * **name** -- human-readable agent name * **skills** -- list of capabilities with input/output schemas * **capabilities** -- supported features (streaming, multi-turn, etc.) * **endpoint** -- the agent's A2A service URL A2A messages contain typed parts: | Part Type | Usage | | ---------- | --------------------------------------------------------- | | `TextPart` | Natural-language text (questions, analysis, explanations) | | `DataPart` | Structured data (datasource configs, query parameters) | | `FilePart` | Binary artifacts (charts, exported files) | Agents emit events for real-time frontend updates: | Event | Purpose | | ------------------------- | -------------------------------------------------------------- | | `TaskStatusUpdateEvent` | Progress messages ("Analyzing schema...", "Generating SQL...") | | `TaskArtifactUpdateEvent` | Final results (data tables, charts, text analysis) | Every pipeline step emits a status event before starting work, providing real-time visibility into the agent's reasoning process. The A2A `context_id` maps to the session ID for multi-turn conversation state. This enables agents to maintain context across multiple questions in the same conversation. ## Pipeline Framework Agent workflows are built on the `commons.pipeline.Pipeline` state-machine framework: * **Steps** are named functions that perform a unit of work * Each step returns a `Transition` object with a `goto` target (next step, `break`, or `error`) * The pipeline supports **cooperative cancellation** for graceful shutdown * Unexpected exceptions are wrapped in `StepError` for structured error handling The Text2SQL agent uses this framework for its generate-validate-execute flow: ```text theme={null} generate_sql -> validate_sql -> execute_query -> format_results ``` ## LLM Integration Data Insights uses **LiteLLM** for provider-agnostic LLM access: | Feature | Details | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Client** | `commons.llm.LLMClient` wrapping LiteLLM | | **Model format** | `provider/model` (e.g., `vertex_ai/gemini-3.5-flash`, `vertex_ai/claude-opus-4-8`, `openai/gpt-5.5`) | | **Observability** | Langfuse LLM tracing auto-enabled when `LANGFUSE_HOST` is set. Chat dispatch and the pipeline executor are auto-instrumented with `@observe` decorators, A2A trace context propagates across services so a single conversation produces one unified trace, trace IDs are seeded deterministically from `turn_id`, and per-iteration spans are emitted inside agent loops. See [Langfuse Setup](/guides/langfuse-setup) for configuration and [Langfuse Overview](/deployment/observability/langfuse) for the full tracing model. | | **Configuration** | Per-service LLM env vars (`TALK2DATA_TEXT2SQL_LLM_MODEL`, `TALK2DATA_INSIGHTS_LLM_MODEL`, etc.) via `pydantic_settings.BaseSettings` | Credentials are never hardcoded. All API keys and connection strings are loaded from environment variables or `.env` files via `pydantic_settings.BaseSettings`. ## Database Schema The `talk2data` database schema stores conversation state and artifacts: | Table | Purpose | | -------------------------- | -------------------------------------------------------- | | **sessions** | Chat sessions with user and project context | | **conversation\_messages** | Individual messages within a session | | **artifacts** | Generated outputs (SQL queries, results, visualizations) | | **feedback** | User feedback on agent responses | * **Primary keys**: UUID strings * **Timestamps**: `DateTime(timezone=True)` with UTC * **ORM**: SQLAlchemy 2.0+ async with `asyncpg` driver * **Migrations**: Alembic in `packages/common-db/` ## REST Endpoints The Talk2Data Service exposes the following REST and SSE endpoints: | Endpoint | Purpose | | ------------------------------ | ----------------------------------------------------------- | | `GET /talk2data/chat/sessions` | List active chat sessions | | `POST /talk2data/chat/*` | Start or continue a chat session (SSE streaming) | | `POST /talk2data/v1/sample` | Preview rows from a data connection table (no LLM involved) | The `/talk2data/v1/sample` endpoint is useful for data exploration before composing a question — it fetches a configurable number of rows from a named table via an existing data connection, using a fully qualified table name (`database.schema.table`). See [Text-to-SQL](/data-insights/text-to-sql) for request and error details. ## Platform Integration Data Insights integrates with the platform layer for: | Capability | Platform Service | | ------------------ | ---------------------------------------------------------------- | | **Authentication** | Governance (JWT validation via the platform identity provider) | | **Authorization** | Governance (permission checks via the authorization service SDK) | \| **Data connections** | Assets (configured database connections) | \| **Asset management** | Assets (data connections, artifacts, files, models) | The integration uses auto-generated Python SDKs from the platform's OpenAPI specs. Data Insights never implements its own permission checks. ## Next Steps Learn how to use the conversational interface for data analysis. Understand how natural-language questions are converted to SQL queries. See how Data Insights agents are registered and discovered. Configure data connections for your databases. # Text-to-SQL Source: https://docs.emergence.ai/data-insights/text-to-sql How the Text2SQL agent converts natural-language questions into validated, executable SQL using schema-aware generation and sqlglot validation. # Text-to-SQL Agent The Text2SQL Agent is a specialized A2A agent that converts natural-language questions into SQL queries. It uses schema-aware generation, SQL validation via `sqlglot`, and safe execution against customer databases. ## Pipeline versions Two Text2SQL pipelines are available, controlled by a single configuration switch: * **Legacy pipeline (default, in production)**: the generate → validate → execute flow described below. A single schema is fetched per question, validated, and executed. * **Multi-schema pipeline (in rollout, behind a flag)**: introduces schema selection and enrichment steps so questions spanning multiple schemas are handled without requiring the user to pre-select a schema. The architecture is decided (ADRs 004/005) and the scaffolding is in place; production rollout is in progress. Switch via `text2sql.pipeline.use_new_pipeline` in `config.yaml`. The legacy pipeline is the supported path in current deployments. The multi-schema pipeline introduces: * **Schema table selection** (ADR 004): a schema-selection step narrows the catalog to the schemas relevant to the question, using semantic search over the data catalog. * **Raw DDL storage** (ADR 005): column-level DDL is stored and retrieved for the selected schemas, giving the LLM richer type and constraint context without fetching the entire catalog. * `CatalogSearchResult` model and pipeline state fields (`pruned_schema`, `enriched_schema`, etc.) carrying multi-schema context across steps. * A new `allow_multi_schema` flag on `CatalogSchemaProvider` that bypasses the single-schema guard when the pipeline is active. The rest of this page describes the legacy pipeline, which remains the default. ## Pipeline Overview ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR Q["NL Question\n(from user)"] SR["Schema\nRetrieval"] LLM["LLM\n(SQL generation)"] VAL{"sqlglot\nValidation"} EXEC["Query\nExecution\n(read-only)"] FMT["Result\nFormatting\n(DataPart)"] A2A["A2A Event\nStream"] ERR["Error\nFeedback"] Q --> SR --> LLM --> VAL VAL -->|"valid"| EXEC --> FMT --> A2A VAL -->|"invalid — retry"| ERR --> LLM classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef llm fill:#CC79A7,stroke:#555555,color:#000 classDef val fill:#F0E442,stroke:#555555,color:#000 classDef db fill:#0072B2,stroke:#555555,color:#fff classDef out fill:#009E73,stroke:#555555,color:#000 classDef err fill:#D55E00,stroke:#555555,color:#000 class Q,SR entry class LLM llm class VAL val class EXEC db class FMT,A2A out class ERR err ``` The Text2SQL agent follows a **generate, validate, execute** pipeline built on the `commons.pipeline.Pipeline` framework: The agent fetches the target database schema (tables, columns, types, relationships, foreign keys) from the data connection. This schema context is included in the LLM prompt for accurate SQL generation. The LLM generates a SQL query based on the natural-language question and schema context. The prompt includes: * Database schema (tables, columns, types) * Table relationships and foreign keys * Previous conversation context (for follow-up questions) * Database dialect (PostgreSQL) The generated SQL is validated using `sqlglot`: * **Syntax validation**: Ensures the SQL is syntactically correct * **Dialect validation**: Confirms compatibility with the target database dialect * **Safety checks**: Prevents destructive operations (DROP, DELETE, TRUNCATE, ALTER) * If validation fails, the agent retries generation with the error feedback The validated SQL is executed against the customer database via the data connection. The agent uses a read-only connection with query timeouts to prevent runaway queries. Query results are formatted as structured data (DataPart) and returned to the calling agent via A2A events. ## Schema-Aware Generation The quality of generated SQL depends heavily on the schema context provided to the LLM. The Text2SQL agent builds a comprehensive schema representation: ```python theme={null} # Schema context includes: # - Table names and descriptions # - Column names, types, and constraints # - Primary and foreign key relationships # - Sample values for enum-like columns # - Table-level comments and documentation ``` For better SQL generation quality, add comments and descriptions to your database schema. The Text2SQL agent uses these to understand the semantic meaning of tables and columns. ## SQL Validation The agent uses `sqlglot` for multi-layer validation: | Check | Purpose | | --------------------- | ------------------------------------------------- | | **Parse** | Verify the SQL is syntactically valid | | **Transpile** | Confirm compatibility with the PostgreSQL dialect | | **Safety** | Block destructive statements (DDL, DML mutations) | | **Column validation** | Verify referenced columns exist in the schema | ### Safety Rules The Text2SQL agent enforces strict safety rules: | Allowed | Blocked | | ------------------- | ------------------------------------- | | `SELECT` queries | `INSERT`, `UPDATE`, `DELETE` | | `WITH` (CTEs) | `DROP`, `ALTER`, `TRUNCATE` | | Aggregate functions | `CREATE`, `GRANT`, `REVOKE` | | Window functions | Raw `EXECUTE`, dynamic SQL | | Subqueries | Statements that modify data or schema | ## Pipeline Framework The Text2SQL agent uses the `commons.pipeline.Pipeline` state machine for managing the generate-validate-execute flow: ```python theme={null} from commons.pipeline import Pipeline, Transition, StepError class Text2SQLPipeline(Pipeline): async def generate(self, context): sql = await self.llm.generate_sql(context.question, context.schema) return Transition(goto="validate", data={"sql": sql}) async def validate(self, context): try: validated = sqlglot.transpile(context.sql, read="postgres") return Transition(goto="execute", data={"sql": validated}) except sqlglot.errors.ParseError as e: # Retry generation with error feedback return Transition(goto="generate", data={"error": str(e)}) async def execute(self, context): results = await self.db.execute(context.sql) return Transition(goto="break", data={"results": results}) ``` Steps return `Transition` objects that specify the next step (`goto`), with `"break"` signaling pipeline completion and `"error"` signaling failure. Built-in cooperative cancellation allows long-running pipelines to be interrupted gracefully. ## Event Streaming The Text2SQL agent emits A2A events at each pipeline step for real-time progress: | Step | Event | Message | | ---------------- | ------------------------- | ------------------------------ | | Schema retrieval | `TaskStatusUpdateEvent` | "Analyzing database schema..." | | SQL generation | `TaskStatusUpdateEvent` | "Generating SQL query..." | | Validation | `TaskStatusUpdateEvent` | "Validating query..." | | Execution | `TaskStatusUpdateEvent` | "Executing query..." | | Results | `TaskArtifactUpdateEvent` | Query results as DataPart | ## Error Handling The agent uses typed error hierarchies for recoverable failures: | Error Type | Handling | | ---------------------- | ------------------------------------------------------ | | `SQLValidationError` | Retry SQL generation with validation error context | | `QueryExecutionError` | Return user-friendly error message with the failed SQL | | `SchemaRetrievalError` | Check data connection health and credentials | | `QueryTimeoutError` | Suggest query simplification or filtering | | Unexpected errors | Wrapped in `StepError` by the pipeline framework | ## Agent Card The Text2SQL agent exposes its capabilities via the standard A2A Agent Card: ```json theme={null} { "name": "text2sql", "description": "Converts natural-language questions to SQL queries", "url": "http://text2sql:8001", "skills": [ { "id": "nl-to-sql", "name": "Natural Language to SQL", "description": "Generate and execute SQL from natural-language questions", "tags": ["sql", "database", "analytics"] } ] } ``` ## Sample Data Preview The Talk2Data Service exposes a `POST /talk2data/v1/sample` endpoint that returns a preview of rows from a database table without running a full analytical query. This is used by the frontend to let users inspect table contents before composing a natural-language question. **Request fields:** | Field | Type | Description | | -------------- | ------- | ------------------------------------------------------------ | | `table_fqn` | string | Fully qualified table name: `database.schema.table` | | `resource_uri` | string | Data connection resource URI (from the Assets service) | | `limit` | integer | Number of rows to return (default varies by connection type) | **Error responses:** | Status | Cause | | ------ | ------------------------------------------------------------ | | `400` | Invalid `table_fqn` format (must be `database.schema.table`) | | `404` | Data connection not found for the given `resource_uri` | | `501` | Connection type does not support row sampling | | `503` | `EM_RUNTIME_ASSETS_URL` not configured | The endpoint authenticates via the standard platform JWT and respects project-scoped data connection access. ## MCP API changes (2026-05-13) Two breaking changes shipped in em-talk2data v3.2.0 (commit `ecdc18a`): ### generate-sql: schema field is now required `GenerateSQLRequest` changed from an optional list to a required single object: ```json Before (schemas — optional list) theme={null} { "question": "How many orders shipped last month?", "database": "analytics", "schemas": [{ "tables": [...] }] } ``` ```json After (schema — required single object; database required) theme={null} { "question": "How many orders shipped last month?", "database": "analytics", "schema": { "tables": [...] } } ``` * The JSON field name is `schema` (not `schema_` — that is the Python attribute alias) * The field is **required**; omitting it returns `422 Unprocessable Entity` with an `HTTPValidationError` body ### execute-query: 422 responses now include error detail `execute-query` previously returned a generic 422 with no body schema on SQL validation failures. It now returns a structured `HTTPValidationError` body, making it easier to surface validation failure reasons in MCP clients. ### Deployment: missing env vars in local/CI Helm values `EM_RUNTIME_ASSETS_URL` and `EM_RUNTIME_UTILS_URL` were missing from the `talk2data-service` env block in `values.local.yaml` and `values.ci.yaml`. This caused `execute_query` calls to return `503`. Update local and CI overrides to include: ```yaml theme={null} talk2data-service: env: EM_RUNTIME_ASSETS_URL: "http://em-runtime-assets:8000" # local: subchart EM_RUNTIME_UTILS_URL: "http://em-runtime-utils:8000" # local: subchart ``` In GKE (`values.yaml`), the URLs use the cross-namespace service DNS (`em-runtime-assets.em-runtime.svc.cluster.local`) — these were already correct before this fix. ## Next Steps See how Text2SQL fits into the full conversational analytics pipeline. Learn how the Insights Agent orchestrates analysis beyond SQL. Understand how query results become interactive charts. Connect a database for Text2SQL to query. # Visualizations Source: https://docs.emergence.ai/data-insights/visualizations How CRAFT Insights Agent generates interactive Plotly charts from query results to visualize data patterns and trends. # Visualizations The Data Insights solution generates interactive **Plotly** visualizations as part of the conversational analytics pipeline. When a user's question benefits from a visual answer, the Insights Agent automatically selects and generates the appropriate chart type. ## How Visualizations Are Generated The Insights Agent determines when a visualization would enhance the answer and generates it as part of the agentic loop: The Insights Agent evaluates whether the question implies a visual answer. Questions about trends, comparisons, distributions, or proportions typically trigger visualization. The Text2SQL Agent generates and executes a query to retrieve the data needed for the visualization. The LLM selects the most appropriate chart type based on the data shape, question intent, and number of dimensions: | Data Pattern | Chart Type | | ------------------- | --------------------- | | Trends over time | Line chart | | Category comparison | Bar chart | | Part-to-whole | Pie / Donut chart | | Distribution | Histogram | | Correlation | Scatter plot | | Multi-dimensional | Grouped / Stacked bar | The Insights Agent generates Python code using Plotly to create the visualization: ```python theme={null} import plotly.express as px fig = px.bar( df, x="product_category", y="revenue", title="Revenue by Product Category", color="region", barmode="group" ) fig.update_layout( xaxis_title="Category", yaxis_title="Revenue ($)" ) ``` The chart is rendered and delivered as an artifact via the A2A protocol. The frontend renders it as an interactive Plotly chart with hover tooltips, zoom, and pan capabilities. ## Supported Chart Types | Chart | When Used | Plotly Function | | --------------- | ----------------------------- | ---------------- | | **Line** | Time series, trends | `px.line()` | | **Bar** | Category comparison | `px.bar()` | | **Pie / Donut** | Proportions (\< 8 categories) | `px.pie()` | | **Scatter** | Correlations, distributions | `px.scatter()` | | **Histogram** | Value distributions | `px.histogram()` | | **Area** | Cumulative trends | `px.area()` | | Chart | When Used | Plotly Function | | --------------- | ----------------------------- | ------------------------- | | **Grouped bar** | Multi-category comparison | `px.bar(barmode="group")` | | **Stacked bar** | Part-to-whole over categories | `px.bar(barmode="stack")` | | **Heatmap** | Matrix data, correlations | `px.imshow()` | | **Box plot** | Statistical distributions | `px.box()` | | **Treemap** | Hierarchical proportions | `px.treemap()` | | **Funnel** | Sequential stages | `px.funnel()` | ## Interactive Features Plotly charts rendered in the frontend support full interactivity: * **Hover tooltips**: View exact values for each data point * **Zoom and pan**: Focus on specific regions of the chart * **Legend toggle**: Show/hide individual series by clicking the legend * **Download**: Export as PNG or SVG directly from the chart toolbar * **Responsive sizing**: Charts adapt to the container width ## Chart Generation Examples **Question:** "Show me the monthly revenue trend for the past year" The Insights Agent generates a line chart with months on the x-axis and revenue on the y-axis, including trend annotations for significant changes. **Question:** "Compare sales across product categories by region" The agent generates a grouped bar chart with categories on the x-axis, sales on the y-axis, and color-coded bars for each region. **Question:** "What does the distribution of order values look like?" The agent generates a histogram showing the frequency distribution of order values, with optional statistics overlay (mean, median, standard deviation). **Question:** "What percentage of revenue comes from each channel?" The agent generates a pie or donut chart showing revenue proportions by channel, with percentage labels. ## Artifact Storage Generated visualizations are stored as session artifacts in the Talk2Data database: | Artifact Component | Storage | | ----------------------- | --------------------------------------------- | | **Chart specification** | Plotly JSON stored in the artifacts table | | **Underlying data** | Query results stored alongside the chart | | **Python code** | Generation code preserved for reproducibility | | **Metadata** | Chart type, title, axes, timestamp | Artifacts persist for the duration of the session and can be referenced in follow-up questions ("Make that a line chart instead" or "Add a trend line"). ## Customization in Follow-Up Questions Users can refine visualizations through natural-language follow-ups: | Request | Result | | ------------------------- | ------------------------------- | | "Make that a bar chart" | Chart type changed | | "Show only the top 5" | Data filtered, chart updated | | "Add a trend line" | Trend line overlay added | | "Use a logarithmic scale" | Y-axis scale changed | | "Split by region" | Color dimension added | | "Show as a percentage" | Values converted to proportions | ## Next Steps See the full conversational pipeline that produces visualizations. Understand how data is retrieved before visualization. Learn about the reasoning engine that selects chart types. Connect a database to start visualizing your data. # Helm Configuration Source: https://docs.emergence.ai/deployment/helm/configuration Configure the em-runtime Helm chart for development, staging, and production environments with external databases, S3 storage, and HTTPS. # Helm Configuration The em-runtime Helm chart packages all platform services and their dependencies into a single deployable unit. This guide covers the chart architecture, deployment modes, secret management, and production configuration. ## Chart Overview The chart deploys and manages the following components: | Component | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **em-runtime-governance** | Organizations, projects, and permissions | | **em-runtime-assets** | Artifacts, data connections, files, and models | | **em-runtime-utils** | Data catalog, scheduling, context packs, and memories | | **Keycloak** | Identity and access management (multi-tenant realms) | | **OpenFGA** | Fine-grained authorization (Zanzibar model) | | **Infisical** | Secrets management (optional; can be replaced by ESO + GCP Secret Manager, see [Secrets Management](/security/secrets-management)) | | **PostgreSQL** | Persistent data storage (optional, in-cluster) | | **Redis** | Caching and session storage (optional, in-cluster) | ## Deployment Modes All dependencies (PostgreSQL, Redis) are deployed as part of the Helm release within the cluster. ```bash theme={null} helm install em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version $VERSION \ --namespace=$NAMESPACE ``` Best for development, testing, and quick setup. PostgreSQL and Redis run outside the cluster (managed services or self-hosted). The chart connects via configuration. ```bash theme={null} helm install em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version $VERSION \ --namespace=$NAMESPACE \ --set postgres.enabled=false \ --set redis.enabled=false \ --set existingSecret=em-runtime-external-credentials ``` Best for production with managed database services. ## Secret Management All platform credentials are consolidated into a single chart-managed Secret named `em-runtime-secrets`. In external mode, you provide base connection details, and the chart auto-computes derived values. ### External Credentials Secret Create this before installing the chart when using external databases: ```bash theme={null} kubectl create secret generic em-runtime-external-credentials \ --namespace=$NAMESPACE \ --from-literal=postgres-password='' \ --from-literal=postgres-host='' \ --from-literal=postgres-port='5432' \ --from-literal=postgres-username='postgres' \ --from-literal=redis-password='' \ --from-literal=redis-host='' \ --from-literal=redis-port='6379' \ --from-literal=redis-scheme='redis' \ --from-literal=redis-username='default' ``` Set `redis-scheme` to `rediss` (double `s`) for TLS connections (e.g., GCP Memorystore with in-transit encryption). ### S3 Storage Credentials ```bash theme={null} kubectl create secret generic em-runtime-s3 \ --namespace=$NAMESPACE \ --from-literal=access-key-id='' \ --from-literal=secret-access-key='' ``` Skip this step if using IRSA (AWS) or GCP Workload Identity. ## External Object Storage For production, use S3-compatible object storage instead of the default PVC: ```yaml theme={null} em-runtime-assets: storage: enabled: false # Disable PVC extraEnvVars: - name: STORAGE_TYPE value: "s3" - name: S3_REGION value: "us-east-1" - name: S3_BUCKET_NAME value: "my-bucket" - name: S3_PREFIX_UPLOADED value: "uploaded" - name: S3_PREFIX_GENERATED value: "generated" - name: S3_ACCESS_KEY_ID valueFrom: secretKeyRef: { name: em-runtime-s3, key: access-key-id } - name: S3_SECRET_ACCESS_KEY valueFrom: secretKeyRef: { name: em-runtime-s3, key: secret-access-key } ``` ### Provider-Specific Notes | Provider | Configuration | | ------------------------ | ------------------------------------------------------------------------------------------------ | | **AWS S3** | No `S3_ENDPOINT_URL` needed. Set `S3_REGION` to your bucket's region. | | **Google Cloud Storage** | Use HMAC keys. Set `S3_ENDPOINT_URL` to `https://storage.googleapis.com`, `S3_REGION` to `auto`. | | **MinIO** | Set `S3_ENDPOINT_URL` to your MinIO endpoint (e.g., `http://minio:9000`). | ## Enabling HTTPS EM-Runtime uses Gateway API for routing. TLS termination is configured on the Gateway resource using cert-manager. Install cert-manager with Gateway API support enabled (`config.enableGatewayAPI=true`). Create a ClusterIssuer for your ACME provider (e.g., Let's Encrypt). Add `cert-manager.io/cluster-issuer` annotation and configure an HTTPS listener on your Gateway resource. ## Service URL Environment Variables Service-to-service communication uses canonical URL environment variables. These names were standardized as part of PE-200; legacy aliases are retained as temporary hotfixes for older application versions. | Canonical name | Replaces (legacy) | Purpose | | ----------------------- | ------------------------------------------- | ----------------------- | | `KEYCLOAK_URL` | `AUTH_URL`, `EM_RUNTIME_KEYCLOAK_URL` | Keycloak base URL | | `EM_RUNTIME_ASSETS_URL` | `RUNTIME_URL` (when used for asset routing) | Assets service base URL | | `EM_RUNTIME_UTILS_URL` | `RUNTIME_URL` (when used for utils routing) | Utils service base URL | Application versions still on the legacy names continue to work because both are set during the rollout. Once all consumer applications upgrade past the migration point, the legacy aliases will be removed. ### UI compliance and external link variables The Runtime UI surfaces compliance links (Terms of Service, Privacy Policy, DPA) and an embedded analytics script. Configure them via env vars on the `em-runtime-ui` deployment: | Variable | Purpose | | -------------------------- | ------------------------------------------------------------------- | | `YIELD_INSIGHTS_URL` | URL of the Yield Insights solution (semiconductor deployments only) | | `TERMS_OF_SERVICE_URL` | Terms of Service link surfaced in the UI footer | | `PRIVACY_POLICY_URL` | Privacy Policy link surfaced in the UI footer | | `EMERGENCE_DPA_URL` | Data Processing Agreement link | | `EMERGENCE_DPA_UPDATE_URL` | DPA update notification link | | `TERMLY_SCRIPT_SRC` | Termly script source URL for compliance banner | These are configured via the `em-runtime-ui.env` block in `values.yaml`, the same way as service URLs above. ## Environment Variable Override System Each runtime service has three layers for environment variables: | Layer | Format | Purpose | | -------------- | ----------------------- | ------------------------------------------------------------------------- | | `env` | `map[string]string` | Simple key-value pairs. **Use this to override defaults.** | | `envVars` | List of K8s env objects | Variables using `valueFrom` (secrets, config maps). Defined by the chart. | | `extraEnvVars` | List of K8s env objects | Additional variables appended after `envVars`. | ### Override Precedence ```text theme={null} env (wins) --> envVars (skipped if name exists in env) --> extraEnvVars (skipped if name exists in env) ``` **Example -- override defaults across services:** ```yaml theme={null} em-runtime-governance: env: DOCS_ENABLED: "false" # Disable API docs LOG_LEVEL: "WARNING" # Reduce log verbosity CORS_ENABLED: "false" # Disable CORS em-runtime-assets: env: DOCS_ENABLED: "false" LOG_LEVEL: "WARNING" em-runtime-utils: env: DOCS_ENABLED: "false" LOG_LEVEL: "WARNING" ``` ## Bootstrap Configuration On first startup, the platform bootstraps a default organization: ```yaml theme={null} bootstrap: organizationId: "acme" # Also used as Keycloak realm name organizationName: "ACME Corp" organizationDescription: "ACME Corporation platform" adminEmail: "admin@acme.com" ``` The bootstrap is **idempotent** -- it checks for existing resources before creating. The `organizationId` must contain only alphanumeric characters, hyphens, or underscores. ## Production Values File Below is a sample `production-values.yaml`: ```yaml theme={null} # External hostname and scheme hostname: "api.example.com" scheme: "https" # Bootstrap organization bootstrap: organizationId: "my-org" organizationName: "My Organization" adminEmail: "admin@example.com" # External databases existingSecret: "em-runtime-external-credentials" postgres: enabled: false redis: enabled: false # Keycloak keycloak: replicaCount: 2 resources: requests: { memory: "1536Mi", cpu: "1000m" } limits: { memory: "2Gi", cpu: "2000m" } # OpenFGA with HPA openfga: replicaCount: 2 autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 75 # Governance with HPA em-runtime-governance: replicaCount: 2 autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 75 env: LOG_LEVEL: "WARNING" DOCS_ENABLED: "false" # Assets with HPA and S3 em-runtime-assets: replicaCount: 2 autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 storage: enabled: false env: LOG_LEVEL: "WARNING" DOCS_ENABLED: "false" extraEnvVars: - name: STORAGE_TYPE value: "s3" - name: S3_REGION value: "us-east-1" - name: S3_BUCKET_NAME value: "my-org-em-runtime" # Utils with HPA em-runtime-utils: replicaCount: 2 autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 env: LOG_LEVEL: "WARNING" DOCS_ENABLED: "false" ``` ### Install with Production Values ```bash theme={null} helm install em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version $VERSION \ --namespace production \ -f production-values.yaml ``` ## Testing The chart includes a Helm test hook that runs end-to-end tests: ```bash theme={null} helm test $RELEASE --namespace $NAMESPACE --timeout 10m ``` The test job creates a temporary organization, validates role-based access across all permission groups, and cleans up. ## Backups Back up these components together for a consistent restore: | Component | Contains | Backup Method | | ------------------------------ | ------------------------------------------- | ------------------------------------------------------- | | **PostgreSQL** | All service data | `pg_dumpall` or cloud snapshots | | **Redis** | Session and cache data | `redis-cli BGSAVE` or cloud snapshots | | **em-runtime-secrets** | All platform credentials | `kubectl get secret em-runtime-secrets -o yaml` | | **infisical-bootstrap-secret** | Machine identity token (if using Infisical) | `kubectl get secret infisical-bootstrap-secret -o yaml` | | **Assets storage** | Uploaded/generated artifacts | VolumeSnapshots or S3 bucket backup | If using Infisical, the `em-runtime-secrets` secret contains the `ENCRYPTION_KEY` and `AUTH_SECRET`. Without these, encrypted data stored by Infisical cannot be decrypted. If using ESO + GCP Secret Manager, secrets are sourced from GCP SM directly, back up the GCP Secret Manager secrets instead. ## Next Steps Complete reference for all Helm chart values. Version upgrades, migration steps, and rollback procedures. Configure telemetry for all runtime services. # em-core Chart Source: https://docs.emergence.ai/deployment/helm/em-core Helm chart for deploying the CRAFT core platform — em-platform, em-user-service, em-user-portal, Keycloak, PostgreSQL and Redis. # em-core Chart `em-core` deploys the CRAFT core platform to a Kubernetes cluster. It includes all services required to run the platform's core capabilities. ## Components | Component | Image | Purpose | | ----------------- | ------------------------------------- | ---------------------------------------- | | `em-platform` | `ghcr.io/emergenceai/em-platform` | Main backend with workflow orchestration | | `em-user-service` | `ghcr.io/emergenceai/em-user-service` | User management and authentication | | `em-user-portal` | `ghcr.io/emergenceai/em-user-portal` | Frontend web application | | `keycloak` | upstream Keycloak | Identity and access management | | `postgresql` | `postgres:alpine` | Persistent data storage | | `redis` | `redis:alpine` | In-memory caching and session storage | | `nginx` | upstream Nginx | Ingress and load balancing | ## Quick start ```bash theme={null} export HOSTNAME=platform.example.com export KEYCLOAK_ADMIN_PASSWORD= export POSTGRES_PASSWORD= ``` ```bash theme={null} kubectl create namespace em-core ``` ```bash theme={null} kubectl create secret generic em-core-credentials \ --namespace em-core \ --from-literal=KEYCLOAK_ADMIN_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD \ --from-literal=POSTGRES_PASSWORD=$POSTGRES_PASSWORD ``` ```bash theme={null} helm install em-core oci://ghcr.io/emergenceai/em-charts/em-core \ --version 0.0.8 \ --namespace em-core \ --set hostname=$HOSTNAME \ --set postgresql.existingSecret=em-core-credentials \ --set redis.existingSecret=em-core-credentials ``` ## Using external PostgreSQL and Redis For production deployments, disable the bundled databases and connect to managed instances: ```yaml theme={null} # values.yaml postgresql: enabled: false redis: enabled: false postgresql: existingSecret: my-external-db-credentials # Secret must contain: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD redis: existingSecret: my-external-db-credentials # Secret must contain: REDIS_HOST, REDIS_PORT ``` ## Enabling HTTPS Install [cert-manager](https://cert-manager.io) and configure a ClusterIssuer, then: ```yaml theme={null} hostname: platform.example.com schema: https ingress: tls: enabled: true issuer: letsencrypt-prod ``` ## Key values | Value | Default | Description | | ---------------------------------- | ------------------------------------- | --------------------------------------------------- | | `hostname` | `localhost` | External hostname for ingress and service discovery | | `schema` | `http` | `http` or `https` | | `em-platform.image.repository` | `ghcr.io/emergenceai/em-platform` | em-platform image | | `em-user-service.image.repository` | `ghcr.io/emergenceai/em-user-service` | em-user-service image | | `em-user-portal.image.repository` | `ghcr.io/emergenceai/em-user-portal` | em-user-portal image | | `postgresql.enabled` | `true` | Deploy bundled PostgreSQL | | `redis.enabled` | `true` | Deploy bundled Redis | The full values reference is in `charts/em-core/values.yaml` in the em-charts repository. ## Related Extends em-core with data analysis and insights capabilities. Base chart used by em-core for each component. Chart version upgrade procedures. # em-data-insights Chart Source: https://docs.emergence.ai/deployment/helm/em-data-insights Helm chart for deploying the CRAFT Data Insights solution — extends em-core with data analysis agents and Redshift integration. # em-data-insights Chart `em-data-insights` deploys the CRAFT Data Insights solution — a superset of `em-core` that adds the data analysis agent and Redshift connector. ## Components `em-data-insights` includes all [em-core](./em-core) components plus: | Component | Purpose | | ----------------------------- | ----------------------------------------------- | | `em-data-insights-agent` | Data analysis and insights generation A2A agent | | `em-redshift-query-connector` | AWS Redshift query connector service | ## Quick start ```bash theme={null} export HOSTNAME=platform.example.com export KEYCLOAK_ADMIN_PASSWORD= export POSTGRES_PASSWORD= export REDSHIFT_HOST= export REDSHIFT_USER= export REDSHIFT_PASSWORD= ``` ```bash theme={null} kubectl create namespace em-data-insights ``` ```bash theme={null} # Platform credentials (Keycloak + PostgreSQL) kubectl create secret generic em-data-insights-credentials \ --namespace em-data-insights \ --from-literal=KEYCLOAK_ADMIN_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD \ --from-literal=POSTGRES_PASSWORD=$POSTGRES_PASSWORD # Redshift credentials — must be named 'redshift' with these exact keys kubectl create secret generic redshift \ --namespace em-data-insights \ --from-literal=redshift-host=$REDSHIFT_HOST \ --from-literal=redshift-port=${REDSHIFT_PORT:-5439} \ --from-literal=redshift-database=$REDSHIFT_DATABASE \ --from-literal=redshift-username=$REDSHIFT_USER \ --from-literal=redshift-password=$REDSHIFT_PASSWORD ``` ```bash theme={null} helm install em-data-insights oci://ghcr.io/emergenceai/em-charts/em-data-insights \ --version 0.0.8 \ --namespace em-data-insights \ --set em-core.hostname=$HOSTNAME \ --set em-core.schema=http ``` ## Redshift integration Configure the Redshift connector in your values: ```yaml theme={null} em-redshift-query-connector: env: REDSHIFT_DATABASE: my_database REDSHIFT_PORT: "5439" envVars: - name: REDSHIFT_HOST valueFrom: secretKeyRef: name: redshift key: redshift-host - name: REDSHIFT_USERNAME valueFrom: secretKeyRef: name: redshift key: redshift-username - name: REDSHIFT_PASSWORD valueFrom: secretKeyRef: name: redshift key: redshift-password ``` ## Using external databases Disable bundled PostgreSQL and Redis for production: ```yaml theme={null} postgresql: enabled: false redis: enabled: false postgresql: existingSecret: my-external-db-credentials redis: existingSecret: my-external-db-credentials ``` ## Key values | Value | Default | Description | | ---------------------------------------------- | ------------------------------------------------- | ---------------------------------------------- | | `em-core.hostname` | `localhost` | External hostname (passed to em-core subchart) | | `em-core.schema` | `http` | `http` or `https` | | `em-data-insights-agent.image.repository` | `ghcr.io/emergenceai/em-data-insights-agent` | Agent image | | `em-redshift-query-connector.image.repository` | `ghcr.io/emergenceai/em-redshift-query-connector` | Redshift connector image | | `postgresql.enabled` | `true` | Deploy bundled PostgreSQL | | `redis.enabled` | `true` | Deploy bundled Redis | The full values reference is in `charts/em-data-insights/values.yaml` in the em-charts repository. ## Related Core platform chart that em-data-insights extends. Data Insights solution capabilities. How the Text2SQL agent converts natural-language questions to SQL. # em-service Chart Source: https://docs.emergence.ai/deployment/helm/em-service Base Helm chart for deploying any CRAFT service on-premise — deployment, environment variables, secrets, ingress, HPA and probes. # em-service Chart `em-service` is the base Helm chart for deploying any containerised CRAFT service to a Kubernetes cluster. Every CRAFT solution chart wraps it as a subchart — one instance per component, differentiated by alias. ## Features * Deployment with configurable replicas, update strategy and pod annotations * Three-layer environment variable model with deduplication (see below) * Secret injection via `envFrom` or individual `envVars` entries with `valueFrom` * Ingress with flexible routing (standard Kubernetes Ingress and Gateway API) * Horizontal Pod Autoscaler * Liveness, readiness and startup probes * PersistentVolumeClaim support * Init containers and sidecars * RuntimeClass for specialised runtimes ## Installation ```bash theme={null} helm install my-service oci://ghcr.io/emergenceai/em-charts/em-service \ --version 0.0.15 \ --namespace my-namespace \ --set image.repository=ghcr.io/emergenceai/my-service \ --set image.tag=v1.0.0 ``` **Version pin: `0.0.15`, not `0.0.16`.** Only `0.0.15` is currently published to the OCI registry (`helm pull --version 0.0.16` returns "manifest unknown"). Bump this page when a newer tag is published. The registry is **private** — run `helm registry login ghcr.io` before pulling; access is granted to the Emergence team and Solution/Agent design partners. ## Environment variable model Three layers, applied in order, with deduplication: | Layer | Format | Purpose | | -------------- | -------------------------- | -------------------------------------------------------------------------------------- | | `env` | `map[string]string` | Plain-text variables. **Always wins** over `envVars`/`extraEnvVars` with the same name | | `envVars` | Kubernetes env object list | Full env spec — use `valueFrom` for secrets, ConfigMaps, field refs | | `extraEnvVars` | Kubernetes env object list | Additional variables appended after `envVars` | **Deduplication rule:** if a key in `env` matches the `name` of an `envVars` or `extraEnvVars` entry, the list entry is suppressed. This allows parent charts to define secret-backed defaults while letting operators override with plain values without creating duplicate env entries. ```yaml theme={null} # Typical pattern: secret default, plain override possible env: LOG_LEVEL: "INFO" # plain override envVars: - name: DATABASE_URL valueFrom: secretKeyRef: name: my-service-secrets key: database-url - name: LOG_LEVEL # suppressed — 'env' wins valueFrom: secretKeyRef: name: my-service-secrets key: log-level ``` ## Key values | Value | Default | Description | | --------------------- | -------------- | ------------------------------------------------- | | `replicaCount` | `1` | Number of replicas | | `image.repository` | `""` | Container image repository | | `image.tag` | `""` | Image tag | | `image.pullPolicy` | `IfNotPresent` | Pull policy | | `imagePullSecrets` | `[]` | Image pull secret names | | `env` | `{}` | Plain-text environment variables | | `envVars` | `[]` | Kubernetes env object list (supports `valueFrom`) | | `service.type` | `ClusterIP` | Kubernetes service type | | `service.port` | `8000` | Service port | | `ingress.enabled` | `false` | Enable Kubernetes Ingress | | `resources` | `{}` | CPU/memory requests and limits | | `autoscaling.enabled` | `false` | Enable HPA | The full values reference is in `charts/em-service/values.yaml` in the em-charts repository. ## Using as a subchart For the how-to flavour (multi-component packaging, env overlays, build-and-publish, GitOps), see [Solution Developer Guide › Package and Deploy](/guides/solution-dev/package-and-deploy). This page is the values reference; that page is the workflow. Service charts declare `em-service` as a subchart dependency in `Chart.yaml`: ```yaml theme={null} # charts/my-service/Chart.yaml apiVersion: v2 name: my-service version: 1.0.0 dependencies: - name: em-service alias: api version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts - name: em-service # second component alias: worker version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts ``` Configure each instance under its alias in `values.yaml`: ```yaml theme={null} api: image: repository: ghcr.io/emergenceai/my-service-api tag: v1.0.0 env: PORT: "8080" worker: image: repository: ghcr.io/emergenceai/my-service-worker tag: v1.0.0 ``` ## Related Core platform chart using em-service for platform components. Data insights solution chart extending em-core. em-runtime chart configuration and deployment modes. # Upgrades Source: https://docs.emergence.ai/deployment/helm/upgrades Version upgrade procedures, Helm chart promotion workflow, migration steps, and rollback strategies for the em-runtime platform. # Upgrades This guide covers the em-runtime Helm chart upgrade process, version promotion through environments, migration procedures, and rollback strategies. ## Upgrade Procedures ### Standard Helm Upgrade For direct Helm deployments (not using ArgoCD): ```bash theme={null} # Check current version helm list -n $NAMESPACE # Review what will change helm diff upgrade em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version $NEW_VERSION \ --namespace $NAMESPACE \ -f values.yaml # Perform the upgrade helm upgrade em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version $NEW_VERSION \ --namespace $NAMESPACE \ -f values.yaml ``` ## Pre-Upgrade Checklist Always back up PostgreSQL, Redis, and the `em-runtime-secrets` Secret before upgrading. Before upgrading: 1. **Review the changelog** for breaking changes and required migrations 2. **Back up databases:** ```bash theme={null} # PostgreSQL pg_dumpall -h $PG_HOST -U postgres > backup-pre-upgrade.sql # Kubernetes Secrets kubectl get secret em-runtime-secrets -n $NAMESPACE -o yaml > em-runtime-secrets-backup.yaml kubectl get secret infisical-bootstrap-secret -n $NAMESPACE -o yaml > infisical-bootstrap-backup.yaml ``` 3. **Verify current health:** ```bash theme={null} kubectl get pods -n $NAMESPACE # The gateway-routed root (/api//) returns service metadata with 200 # when the service is healthy. The Kubernetes liveness/readiness probe target # /health lives at the service root, not under the per-service prefix. curl https://$HOSTNAME/api/governance/ ``` 4. **Test in non-production first** -- promote through dev and staging before production ## Database Migrations EM-Runtime services run Alembic database migrations automatically on startup. Each service manages its own database migrations independently. ### Migration Behavior | Aspect | Behavior | | ------------- | ---------------------------------------- | | **Trigger** | Automatic on pod startup | | **Direction** | Forward only (upgrade) | | **Scope** | Per-service, per-database | | **Safety** | Idempotent -- safe to run multiple times | ### Migration Order Migrations are applied in service startup order: ```text theme={null} PostgreSQL ready -> Keycloak (schema) -> OpenFGA (schema) -> Infisical (schema) -> Governance (alembic) -> Assets (alembic) -> Utils (alembic) ``` If a migration fails, the pod will restart and retry. Check pod logs for migration errors before investigating further. ## Rollback ### Helm Rollback ```bash theme={null} # List revision history helm history em-runtime -n $NAMESPACE # Rollback to previous revision helm rollback em-runtime $PREVIOUS_REVISION -n $NAMESPACE ``` ### Database Rollback Considerations Alembic migrations are forward-only in production. If a database migration must be reversed, you must restore from backup. * **Schema-only changes** (add column, add table): Generally safe to keep during rollback; old code ignores new columns * **Data migrations**: Require database restore from pre-upgrade backup * **Destructive migrations** (drop column, rename): Require database restore ### Direct Helm Always specify `--version` in production: ```bash theme={null} helm upgrade em-runtime oci://ghcr.io/emergenceai/charts/em-runtime \ --version 4.8.6 \ --namespace production \ -f production-values.yaml ``` ## Upgrade Troubleshooting Check pod logs for migration errors: ```bash theme={null} kubectl logs -n $NAMESPACE deploy/em-runtime-governance --previous ``` Common causes: * Database migration conflict (two migrations with same parent) * Missing database or extension * Incompatible secret format Init containers wait for dependencies (PostgreSQL, Redis, Keycloak). Check that all infrastructure services are healthy: ```bash theme={null} kubectl get pods -n $NAMESPACE -l app.kubernetes.io/component=database kubectl logs -n $NAMESPACE -l app=keycloak ``` Verify that `hostname` and `scheme` values match your Gateway and DNS configuration: ```bash theme={null} kubectl get secret em-runtime-secrets -n $NAMESPACE \ -o jsonpath='{.data.base-url}' | base64 -d ``` The CMP plugin may need to rebuild the chart dependency. Force a sync: ```bash theme={null} kubectl get application em-runtime -n argocd -o jsonpath='{.status.sync.status}' # If OutOfSync, trigger a hard refresh argocd app get em-runtime --hard-refresh argocd app sync em-runtime ``` ## Next Steps Complete reference for all Helm chart values. # Values Reference Source: https://docs.emergence.ai/deployment/helm/values-reference Complete reference for all configurable Helm chart values in the em-runtime chart, including global settings, per-service configuration, and subchart overrides. # Values Reference This page documents all configurable values in the em-runtime Helm chart. Values are organized by scope: global, per-service, infrastructure subcharts, and testing. ## Global Values | Key | Type | Default | Description | | ------------------ | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `hostname` | string | `"localhost"` | External hostname for the platform. Used in HTTPRoute hostnames, CORS origins, and Keycloak redirect URIs. | | `scheme` | string | `"http"` | URL scheme (`http` or `https`). Combined with `hostname` to form `BASE_URL`. | | `existingSecret` | string | `""` | Name of a pre-existing Secret containing external database credentials. Required when `postgres.enabled` or `redis.enabled` is `false`. | | `gatewayName` | string | `"em-runtime-gateway"` | Name of the Kubernetes Gateway resource that HTTPRoutes reference. | | `gatewayNamespace` | string | `"gateway"` | Namespace where the Gateway resource is deployed. | | `pullPolicy` | string | `"IfNotPresent"` | Default image pull policy for all containers. | | `imagePullSecrets` | list | `[{name: "em-registry-secret"}]` | Default image pull secrets for private registry access. | ## Bootstrap | Key | Type | Default | Description | | ----------------------------------- | ------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `bootstrap.organizationId` | string | `"emergence"` | Default organization identifier (also used as Keycloak realm name). Must be alphanumeric with hyphens or underscores. | | `bootstrap.organizationName` | string | `"Emergence"` | Display name of the default organization. | | `bootstrap.organizationDescription` | string | `"Emergence organization"` | Description of the default organization. | | `bootstrap.adminEmail` | string | `"admin@emergence.local"` | Admin email for platform services bootstrap. | ## CORS | Key | Type | Default | Description | | -------------- | ---- | ------- | ------------------------------------------------ | | `cors.enabled` | bool | `true` | Enables CORS middleware on all runtime services. | ## HTTPRoute | Key | Type | Default | Description | | ------------------- | ---- | --------- | ------------------------------------------------------------------ | | `httpRoute.enabled` | bool | `true` | Creates an HTTPRoute resource. | | `httpRoute.rules` | list | See below | Routing rules mapping external path prefixes to internal services. | ### Default HTTPRoute Rules | Path Prefix | Service | Port | Rewrite | | ----------------- | --------------------- | ---- | ------------- | | `/keycloak` | keycloak | 8080 | None | | `/api/governance` | em-runtime-governance | 8000 | `/governance` | | `/api/assets` | em-runtime-assets | 8000 | `/assets` | | `/api/utils` | em-runtime-utils | 8000 | `/utils` | ## Service Accounts (Bootstrap) | Key | Type | Default | Description | | -------------------------- | ---- | ---------- | ------------------------------------------------------------ | | `serviceAccounts.enabled` | bool | `true` | Enables service account creation during bootstrap. | | `serviceAccounts.services` | list | 5 services | List of service accounts to create in Keycloak master realm. | Default service accounts: `svc-data-readiness`, `svc-readiness`, `svc-scheduler`, `svc-cleanup`, `svc-indexer`. ## Runtime Service Values The following values apply to each of `em-runtime-governance`, `em-runtime-assets`, and `em-runtime-utils`. Replace `` with the service name. ### Deployment | Key | Type | Default | Description | | ------------------------------ | ------ | ----------------------------------------- | ----------------------------------------------------- | | `.replicaCount` | int | `1` | Number of replicas. | | `.image.repository` | string | `"ghcr.io/emergenceai/em-runtime-"` | Container image repository. | | `.image.tag` | string | Chart version | Container image tag. | | `.image.pullPolicy` | string | `"IfNotPresent"` | Image pull policy. | | `.deploymentStrategy` | object | `{type: "Recreate"}` | Update strategy (Assets only, for PVC compatibility). | ### Autoscaling | Key | Type | Default | Description | | ------------------------------------------------------ | ---- | ------- | ----------------------------------- | | `.autoscaling.enabled` | bool | `false` | Enable Horizontal Pod Autoscaler. | | `.autoscaling.minReplicas` | int | `1` | Minimum replicas. | | `.autoscaling.maxReplicas` | int | `10` | Maximum replicas. | | `.autoscaling.targetCPUUtilizationPercentage` | int | `80` | CPU utilization target for scaling. | ### Resources | Key | Type | Default (Governance/Assets) | Default (Utils) | | ------------------------------------- | ------ | --------------------------- | --------------- | | `.resources.requests.cpu` | string | `"250m"` | `"100m"` | | `.resources.requests.memory` | string | `"512Mi"` | `"256Mi"` | | `.resources.limits.cpu` | string | `"1000m"` | `"500m"` | | `.resources.limits.memory` | string | `"1Gi"` | `"512Mi"` | ### Security Context | Key | Type | Default | Description | | ---------------------------------------------------- | ---- | --------- | ----------------------------- | | `.podSecurityContext.runAsNonRoot` | bool | `true` | Enforce non-root execution. | | `.podSecurityContext.runAsUser` | int | `1000` | User ID for the container. | | `.podSecurityContext.fsGroup` | int | `1000` | Filesystem group. | | `.securityContext.readOnlyRootFilesystem` | bool | `true` | Read-only root filesystem. | | `.securityContext.allowPrivilegeEscalation` | bool | `false` | Prevent privilege escalation. | | `.securityContext.capabilities.drop` | list | `["ALL"]` | Drop all Linux capabilities. | ### Health Probes | Key | Type | Default | Description | | ----------------------------------------- | ------ | ----------- | -------------------------------------- | | `.livenessProbe.httpGet.path` | string | `"/health"` | Liveness check path. | | `.livenessProbe.httpGet.port` | int | `8000` | Liveness check port. | | `.readinessProbe.httpGet.path` | string | `"/health"` | Readiness check path. | | `.startupProbe.failureThreshold` | int | `30` | Startup probe attempts before failure. | | `.startupProbe.periodSeconds` | int | `10` | Seconds between startup probe checks. | ### Scheduling | Key | Type | Default | Description | | ------------------------ | ------ | ------- | ----------------------------------- | | `.nodeSelector` | object | `{}` | Node label constraints. | | `.tolerations` | list | `[]` | Toleration rules for tainted nodes. | | `.affinity` | object | `{}` | Pod scheduling affinity rules. | ### Service | Key | Type | Default | Description | | ------------------------------- | ------ | ------------- | -------------------- | | `.service.type` | string | `"ClusterIP"` | Service type. | | `.service.port` | int | `8000` | Service port. | | `.service.annotations` | object | `{}` | Service annotations. | ## Default Environment Variables ### All Services | Key | Default | Description | | ----------------------------- | ------------------------------ | ---------------------------------------------------- | | `ENVIRONMENT` | `"production"` | Runtime environment identifier. | | `LOG_LEVEL` | `"INFO"` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | | `LOG_FORMAT` | `"JSON"` | Log output format. | | `ROOT_PATH` | `"/api"` | API root path prefix. | | `DOCS_ENABLED` | `"true"` | Enable OpenAPI documentation endpoints. | | `OTEL_ENABLED` | `"true"` | Master switch for OpenTelemetry. | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://otel-collector:4317"` | OTLP collector gRPC endpoint. | | `OTEL_TRACES_ENABLED` | `"true"` | Enable distributed tracing. | | `OTEL_METRICS_ENABLED` | `"true"` | Enable metrics export. | | `OTEL_LOGS_ENABLED` | `"true"` | Enable log record export. | | `OTEL_TRACE_SAMPLE_RATE` | `"0.1"` | Trace sampling ratio (0.0-1.0). | ### Assets and Utils Only | Key | Default | Description | | -------------------- | ------------------------------------- | ---------------------------------- | | `GOVERNANCE_API_URL` | `"http://em-runtime-governance:8000"` | Internal governance service URL. | | `AUTH_CHECK_URL` | `"http://keycloak:8080/keycloak"` | Keycloak URL for token validation. | ### Secret-Backed Variables (via `envVars`) These are sourced from `em-runtime-secrets` and can be overridden by setting the same key in `env`: | Key | Services | Description | | ------------------- | -------- | ----------------------------------------------- | | `BASE_URL` | All | Platform base URL (from `hostname` + `scheme`). | | `CORS_ENABLED` | All | CORS middleware toggle. | | `POSTGRES_HOST` | All | PostgreSQL hostname. | | `POSTGRES_PORT` | All | PostgreSQL port. | | `POSTGRES_USER` | All | PostgreSQL username. | | `POSTGRES_PASSWORD` | All | PostgreSQL password. | | `POSTGRES_DB` | All | Per-service database name. | | `REDIS_HOST` | All | Redis hostname. | | `REDIS_PORT` | All | Redis port. | | `REDIS_SCHEME` | All | Redis connection scheme. | | `REDIS_PASSWORD` | All | Redis password. | ## S3 Storage Variables | Variable | Default | Description | | ---------------------- | ------------ | --------------------------------------------- | | `STORAGE_TYPE` | `local` | Storage backend: `local` or `s3`. | | `S3_ENDPOINT_URL` | None | Custom S3 endpoint (required for GCS, MinIO). | | `S3_ACCESS_KEY_ID` | None | Access key ID. | | `S3_SECRET_ACCESS_KEY` | None | Secret access key. | | `S3_REGION` | `us-east-1` | Bucket region. | | `S3_BUCKET_NAME` | `em-runtime` | Bucket name. | | `S3_PREFIX_UPLOADED` | `uploaded` | Prefix for user-uploaded artifacts. | | `S3_PREFIX_GENERATED` | `generated` | Prefix for system-generated artifacts. | ## Assets Storage (PVC) | Key | Type | Default | Description | | ---------------------------------------- | ------ | ------------------------------- | --------------------------------------- | | `em-runtime-assets.storage.enabled` | bool | `true` | Enable PVC storage. Set `false` for S3. | | `em-runtime-assets.storage.size` | string | `"10Gi"` | PVC size. | | `em-runtime-assets.storage.accessModes` | list | `["ReadWriteMany"]` | PVC access modes. | | `em-runtime-assets.storage.mountPath` | string | `"/var/lib/em-runtime/storage"` | Mount path in container. | | `em-runtime-assets.storage.storageClass` | string | `""` | StorageClass name (empty uses default). | ## Infrastructure Subcharts ### Keycloak | Key | Type | Default | Description | | ------------------------------------ | ------ | ---------- | ---------------------------- | | `keycloak.replicaCount` | int | `1` | Number of Keycloak replicas. | | `keycloak.resources.requests.cpu` | string | `"1000m"` | CPU request. | | `keycloak.resources.requests.memory` | string | `"1280Mi"` | Memory request. | | `keycloak.resources.limits.cpu` | string | `"2000m"` | CPU limit. | | `keycloak.resources.limits.memory` | string | `"2Gi"` | Memory limit. | ### OpenFGA | Key | Type | Default | Description | | ------------------------------------------------------- | ------ | --------- | --------------------------- | | `openfga.replicaCount` | int | `1` | Number of OpenFGA replicas. | | `openfga.autoscaling.enabled` | bool | `false` | Enable HPA. | | `openfga.autoscaling.targetCPUUtilizationPercentage` | int | `80` | CPU scaling target. | | `openfga.autoscaling.targetMemoryUtilizationPercentage` | int | `80` | Memory scaling target. | | `openfga.resources.requests.cpu` | string | `"100m"` | CPU request. | | `openfga.resources.requests.memory` | string | `"256Mi"` | Memory request. | ### Infisical (On-Premises / Cloud-Agnostic) Infisical is one of two supported secrets backends. For cloud deployments on GCP, ESO + GCP Secret Manager can be used instead. See [Secrets Management](/security/secrets-management) for a comparison. | Key | Type | Default | Description | | ---------------------------------------------------------- | ------ | --------- | ------------------------------------------------------------------------------- | | `infisical.enabled` | bool | `true` | Enable the Infisical subchart. Set `false` when using ESO + GCP Secret Manager. | | `infisical-standalone.infisical.replicaCount` | int | `1` | Number of Infisical replicas. | | `infisical-standalone.infisical.resources.requests.cpu` | string | `"350m"` | CPU request. | | `infisical-standalone.infisical.resources.requests.memory` | string | `"512Mi"` | Memory request. | ### PostgreSQL (In-Cluster) | Key | Type | Default | Description | | ------------------------------ | ------ | ------- | ------------------------------------------------------- | | `postgres.enabled` | bool | `true` | Enable in-cluster PostgreSQL. Set `false` for external. | | `postgres.replicaCount` | int | `1` | Number of PostgreSQL replicas. | | `postgres.persistence.enabled` | bool | `true` | Enable data persistence. | | `postgres.persistence.size` | string | `"8Gi"` | Persistent volume size. | ### Redis (In-Cluster) | Key | Type | Default | Description | | --------------------------- | ------ | ------- | -------------------------------------------------- | | `redis.enabled` | bool | `true` | Enable in-cluster Redis. Set `false` for external. | | `redis.replicaCount` | int | `1` | Number of Redis replicas. | | `redis.persistence.enabled` | bool | `true` | Enable data persistence. | | `redis.persistence.size` | string | `"8Gi"` | Persistent volume size. | ## Next Steps Deployment modes, secrets, and production setup guide. Version upgrades, migration steps, and rollback. # Networking Source: https://docs.emergence.ai/deployment/infrastructure/networking Configure Gateway API for traffic routing, cert-manager for TLS certificates, and external-dns for automated DNS record management. # Networking CRAFT uses **Gateway API** for traffic routing, **cert-manager** for TLS certificate lifecycle, and **external-dns** for automatic DNS record management. This guide covers the networking stack across cloud and on-premises environments. ## Gateway API EM-Runtime uses the [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/) (v1) for all external traffic routing. Gateway API replaces traditional Ingress with a more expressive, role-oriented model. ### Ingress Flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR Internet["Internet"] --> LB["Load Balancer (HTTPS)"] LB --> GW["Gateway (TLS terminates)"] GW --> Route["HTTPRoute"] Route --> Svc["Service"] Svc --> Pod["Pod"] classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef app fill:#E69F00,stroke:#555555,color:#000 class Internet entry class LB,GW,Route infra class Svc,Pod app ``` ### Gateway Resource The Gateway resource defines the load balancer and listeners. It must exist before the em-runtime chart deploys HTTPRoutes. ```yaml theme={null} apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: em-runtime-gateway namespace: gateway annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: gatewayClassName: nginx # or gke-l7-global-external-managed listeners: - name: https port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - name: platform-tls allowedRoutes: namespaces: from: All - name: http port: 80 protocol: HTTP allowedRoutes: namespaces: from: All ``` ### HTTPRoute Rules The em-runtime Helm chart creates an HTTPRoute with these default rules: | Path Prefix | Backend Service | Port | URL Rewrite | | ----------------- | --------------------- | ---- | ------------------- | | `/keycloak` | keycloak | 8080 | None (pass-through) | | `/api/governance` | em-runtime-governance | 8000 | `/governance` | | `/api/assets` | em-runtime-assets | 8000 | `/assets` | | `/api/utils` | em-runtime-utils | 8000 | `/utils` | ### Gateway Controllers by Platform | Platform | Controller | GatewayClassName | | ----------- | --------------------------------- | -------------------------------- | | **GKE** | GKE Gateway Controller (built-in) | `gke-l7-global-external-managed` | | **EKS** | AWS Load Balancer Controller | `aws-application-load-balancer` | | **AKS** | Azure Application Gateway | `azure-application-gateway` | | **On-Prem** | NGINX Gateway Fabric | `nginx` | | **On-Prem** | Envoy Gateway | `eg` | ### Configuration in Helm ```yaml theme={null} # Chart values for Gateway reference gatewayName: "em-runtime-gateway" gatewayNamespace: "gateway" hostname: "api.example.com" httpRoute: enabled: true rules: - pathPrefix: "/keycloak" serviceName: keycloak servicePort: 8080 - pathPrefix: "/api/governance" rewritePrefix: "/governance" serviceName: em-runtime-governance servicePort: 8000 ``` ## cert-manager [cert-manager](https://cert-manager.io/) automates TLS certificate provisioning and renewal. It supports ACME (Let's Encrypt) and internal CA issuers. ### Certificate Flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR CM["cert-manager"] -->|DNS-01 challenge| DNS["DNS Provider"] CM -->|stores| Secret["K8s Secret"] GW["Gateway"] -->|reads| Secret LE["Let's Encrypt"] -->|verifies| DNS LE -->|issues cert| CM classDef process fill:#E69F00,stroke:#555555,color:#000 classDef ext fill:#CC79A7,stroke:#555555,color:#000 classDef data fill:#009E73,stroke:#555555,color:#000 class CM,GW process class DNS,LE ext class Secret data ``` ### Installation Install cert-manager with Gateway API support: ```bash theme={null} helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set config.enableGatewayAPI=true \ --set crds.enabled=true ``` ### ClusterIssuer Configuration ```yaml theme={null} apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v2.api.letsencrypt.org/directory email: admin@example.com privateKeySecretRef: name: letsencrypt-prod-key solvers: - dns01: cloudDNS: # GCP project: your-project-id # route53: # AWS # region: us-east-1 # azureDNS: # Azure # subscriptionID: ... ``` ```yaml theme={null} apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: internal-ca spec: ca: secretName: internal-ca-key-pair --- # Create the CA key pair first apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: internal-ca namespace: cert-manager spec: isCA: true commonName: "CRAFT Internal CA" secretName: internal-ca-key-pair issuerRef: name: selfsigned kind: ClusterIssuer ``` ### DNS-01 Solver by Provider | Provider | Solver | IAM Requirement | | ------------------ | ---------------- | ----------------------------------- | | **GCP Cloud DNS** | `dns01.cloudDNS` | Workload Identity service account | | **AWS Route 53** | `dns01.route53` | IRSA role with Route 53 permissions | | **Azure DNS** | `dns01.azureDNS` | Workload Identity managed identity | | **On-Prem (BIND)** | `dns01.rfc2136` | TSIG key for dynamic DNS updates | ### Gateway Annotation Annotate the Gateway to trigger automatic certificate provisioning: ```yaml theme={null} metadata: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod ``` cert-manager watches for Gateway resources with this annotation and provisions TLS certificates for each HTTPS listener. ## external-dns [external-dns](https://github.com/kubernetes-sigs/external-dns) synchronizes Kubernetes resources (Gateway, Service, Ingress) with DNS providers, automatically creating A/CNAME records. ### How It Works 1. external-dns watches Gateway and Service resources for hostname annotations 2. When a Gateway gets an external IP, external-dns creates a DNS A record 3. Records are updated or removed as resources change ### Configuration by Provider ```yaml theme={null} provider: google google: project: your-project-id domainFilters: - runtime.dev.emergence.ai policy: sync sources: - gateway-httproute - service ``` Requires a Workload Identity service account with `dns.admin` role. ```yaml theme={null} provider: aws aws: region: us-east-1 domainFilters: - runtime.dev.emergence.ai policy: sync sources: - gateway-httproute - service ``` Requires an IRSA role with Route 53 `ChangeResourceRecordSets` permission. ```yaml theme={null} provider: rfc2136 rfc2136: host: dns.example.com port: 53 zone: example.com tsigKeyname: external-dns tsigSecretAlg: hmac-sha256 policy: sync sources: - gateway-httproute - service ``` Requires a TSIG key configured on the BIND DNS server. ## TLS Requirements | Boundary | Requirement | | ----------------------- | ---------------------------------- | | Client to load balancer | TLS 1.2+ (certificate required) | | Load balancer to pods | HTTP (in-cluster; TLS optional) | | Services to PostgreSQL | SSL recommended (private network) | | Services to Redis | TLS supported (`rediss://` scheme) | ## Network Policies For on-premises deployments, configure Kubernetes NetworkPolicies for defense in depth: ```yaml theme={null} # Restrict data-insights egress to LLM API endpoints apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: talk2data-egress namespace: em-runtime spec: podSelector: matchLabels: app: data-insights policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 443 protocol: TCP ``` Restrict data-insights egress to allowlisted LLM API endpoints (OpenAI, Anthropic, Vertex AI) for SSRF mitigation. ## Firewall Rules Ensure the following traffic is permitted: | Direction | Port | Protocol | Purpose | | -------------------- | ---- | -------- | --------------------------------- | | **Inbound** | 443 | TCP | HTTPS from clients | | **Inbound** | 80 | TCP | HTTP redirect to HTTPS | | **Cluster to DB** | 5432 | TCP | PostgreSQL | | **Cluster to Cache** | 6379 | TCP | Redis | | **Cluster to S3** | 443 | TCP | Object storage | | **Cluster to LLM** | 443 | TCP | LLM API endpoints (data-insights) | ## DNS Architecture One DNS A record pointing the platform hostname to the load balancer IP/CNAME: ```text theme={null} api.example.com → A → ``` For GKE deployments, external-dns creates this automatically when the Gateway receives an IP address. For on-prem, configure the DNS record manually or use external-dns with RFC2136. ## Troubleshooting ```bash theme={null} # Check cert-manager logs kubectl logs -n cert-manager -l app=cert-manager # Check certificate and challenge status kubectl describe certificate -n gateway-system kubectl get challenges -n gateway-system ``` ```bash theme={null} # Check Gateway status kubectl describe gateway em-runtime-gateway -n gateway # Verify the gateway controller is running kubectl get pods -n gateway-system ``` ```bash theme={null} # Check external-dns logs kubectl logs -l app.kubernetes.io/name=external-dns # Verify IAM / Workload Identity kubectl describe sa external-dns -n external-dns ``` ## Next Steps Complete networking prerequisites and firewall rules. # Secrets Infrastructure Source: https://docs.emergence.ai/deployment/infrastructure/secrets Deploy and configure ESO + GCP Secret Manager or Infisical for secrets management in CRAFT. # Secrets Infrastructure This guide covers deploying and configuring the secrets management backend for CRAFT. Two options are supported, choose based on your deployment environment. For a comparison of the two backends and guidance on which to choose, see [Secrets Management](/security/secrets-management). *** ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB subgraph OptionA["Option A: ESO + GCP Secret Manager"] GCP["GCP Secret Manager\n(Terraform provisioned)"] CSS["ClusterSecretStore\n(Workload Identity auth)"] ES["ExternalSecret\n(K8s resource, refresh interval)"] KS["Kubernetes Secret"] SR["Stakater Reloader\n(watches annotations)"] POD1["Pod restart\n(rolling update)"] GCP --> CSS --> ES --> KS --> SR --> POD1 end subgraph OptionB["Option B: Infisical"] INF["Infisical Server\n(Helm subchart)"] MI["Machine Identity\n(credentials in Helm values)"] POD2["Pods fetch secrets\nat startup via env injection"] INF --> MI --> POD2 end classDef gcp fill:#0072B2,stroke:#555555,color:#fff classDef k8s fill:#56B4E9,stroke:#555555,color:#000 classDef infra fill:#E69F00,stroke:#555555,color:#000 classDef pod fill:#009E73,stroke:#555555,color:#000 class GCP gcp class CSS,ES,KS,SR k8s class INF,MI infra class POD1,POD2 pod ``` ## Option A: ESO + GCP Secret Manager For GKE and GCP-based deployments. Infrastructure is provisioned by Terraform; Kubernetes resources are managed by ArgoCD. ### Prerequisites * GKE cluster with Workload Identity enabled * Terraform-provisioned GCP infrastructure * ArgoCD deployed and managing the cluster ### 1. Terraform Provisions GCP Secrets The `gke` Terraform module automatically creates the required secrets in GCP Secret Manager when you run `terraform apply`. No manual secret creation is needed for infrastructure secrets, the module provisions them with placeholder values that you update after provisioning. After running Terraform: 1. Navigate to [GCP Secret Manager](https://console.cloud.google.com/security/secret-manager) in your project 2. Update each provisioned secret with its actual value 3. ESO will sync the updated values into Kubernetes on the next sync cycle ### 2. Install External Secrets Operator ESO can be installed as an ArgoCD-managed application or deployed manually: ```bash theme={null} helm install external-secrets external-secrets/external-secrets \ --namespace external-secrets \ --create-namespace \ --set installCRDs=true ``` ### 3. ClusterSecretStore ESO requires a `ClusterSecretStore` that configures how to authenticate to GCP Secret Manager. This is provisioned by Terraform/ArgoCD: ```yaml theme={null} apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: name: gcp-secret-manager spec: provider: gcpsm: projectID: "your-project-id" # Replace with your GCP project auth: workloadIdentity: clusterLocation: us-central1 clusterName: "your-cluster-name" serviceAccountRef: name: external-secrets namespace: external-secrets ``` ### 4. ExternalSecret Resources Each service has `ExternalSecret` resources that map GCP SM secrets to K8s Secrets. These are managed by ArgoCD in the `kubernetes/em-runtime/` directory: ```yaml theme={null} apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: em-runtime-credentials namespace: em-runtime annotations: reloader.stakater.com/auto: "true" # Enable auto-restart on change spec: refreshInterval: 1h secretStoreRef: name: gcp-secret-manager kind: ClusterSecretStore target: name: em-runtime-credentials creationPolicy: Owner data: - secretKey: postgres-password remoteRef: key: em-runtime # GCP SM secret name property: postgres-password - secretKey: redis-password remoteRef: key: em-runtime property: redis-password ``` ### 5. Install Stakater Reloader Reloader watches K8s Secrets for changes and triggers rolling restarts of annotated pods: ```bash theme={null} helm install reloader stakater/reloader \ --namespace em-runtime \ --set reloader.watchGlobally=false ``` Pods are automatically enrolled when their Deployment or the Secret has the annotation: ```yaml theme={null} metadata: annotations: reloader.stakater.com/auto: "true" ``` ### 6. Verify Sync ```bash theme={null} # Check ExternalSecret status kubectl get externalsecrets -n em-runtime # Check the synced K8s Secret exists kubectl get secret em-runtime-credentials -n em-runtime # Check Reloader is watching kubectl logs -n em-runtime -l app.kubernetes.io/name=reloader | head -20 ``` ### Manual Sync (Immediate Rotation) To force an immediate sync without waiting for the refresh interval: ```bash theme={null} kubectl annotate externalsecret em-runtime-credentials \ --namespace em-runtime \ force-sync="$(date +%s)" \ --overwrite ``` *** ## Option B: Infisical For on-premises and cloud-agnostic deployments. Infisical runs as a Helm subchart within em-runtime. ### Prerequisites * Kubernetes cluster (any CNCF-conformant distribution) * Helm chart deployed with Infisical subchart enabled ### 1. Enable Infisical in Helm Values Infisical is enabled by default. Verify in your values file: ```yaml theme={null} infisical: enabled: true resources: requests: cpu: "250m" memory: "512Mi" limits: cpu: "1000m" memory: "1Gi" ``` ### 2. Bootstrap Infisical On first deployment, Infisical initializes with the admin email configured in Helm: ```yaml theme={null} bootstrap: adminEmail: "admin@your-domain.com" ``` Access the Infisical UI and complete initial setup: ```bash theme={null} kubectl port-forward svc/em-runtime-infisical -n em-runtime 8080:80 # Open http://localhost:8080 ``` ### 3. Create Machine Identity Create a Kubernetes machine identity in Infisical for each service that needs secret access: 1. In the Infisical UI, navigate to **Project → Access Control → Machine Identities** 2. Create a machine identity with the Kubernetes authentication method 3. Configure the service account name and namespace ### 4. Inject Credentials Infisical credentials are passed to services via environment variables in Helm values: ```yaml theme={null} governance: extraEnvVars: - name: INFISICAL_URL value: "http://em-runtime-infisical.em-runtime.svc.cluster.local" - name: INFISICAL_CLIENT_ID valueFrom: secretKeyRef: name: infisical-machine-identity key: clientId - name: INFISICAL_CLIENT_SECRET valueFrom: secretKeyRef: name: infisical-machine-identity key: clientSecret ``` ### 5. Verify ```bash theme={null} # Check Infisical pod is running kubectl get pods -n em-runtime -l app.kubernetes.io/name=infisical # Check governance service logs for Infisical connection kubectl logs -n em-runtime -l app=em-runtime-governance | grep -i infisical ``` *** ## Backup and Recovery ### ESO + GCP Secret Manager GCP Secret Manager has built-in version history. To recover a previous secret value: ```bash theme={null} # List secret versions gcloud secrets versions list your-secret-name --project=your-project-id # Access a specific version gcloud secrets versions access VERSION_ID --secret=your-secret-name --project=your-project-id ``` ### Infisical Back up the Infisical PostgreSQL database (`infisical` database) using standard PostgreSQL backup tools: ```bash theme={null} kubectl exec -n em-runtime deployment/em-runtime-infisical -- \ pg_dump -U infisical infisical > infisical-backup-$(date +%Y%m%d).sql ``` Include this backup in your regular database backup rotation. See [Backup and Restore](/guides/backup-restore) for the full backup procedure. ## Next Steps Concepts, architecture, and backend comparison. Full backup procedures including secrets backends. # Langfuse Source: https://docs.emergence.ai/deployment/observability/langfuse Deploy and configure Langfuse for LLM observability in CRAFT, traces, evaluations, and prompt management. # Langfuse [Langfuse](https://langfuse.com) is an LLM observability platform used by CRAFT to trace LLM calls, manage prompts, and run evaluations. CRAFT services are auto-instrumented to emit traces in the Langfuse format whenever a Langfuse host is configured. Langfuse is **optional**. The platform operates normally without it. When configured, it enables LLM call tracing, evaluation, and prompt versioning. For the upstream Langfuse documentation, see [langfuse.com/docs](https://langfuse.com/docs). ## LLM Observability vs. Infrastructure Observability | | Langfuse | OpenTelemetry | | ----------------------- | ------------------------------------------------------------------- | -------------------------------------------- | | **Purpose** | LLM call tracing, evaluation, cost | Infrastructure metrics, request traces, logs | | **What it instruments** | LLM API calls (tokens, latency, cost, quality) | Application services (HTTP, DB, Redis) | | **Integration** | LiteLLM callbacks plus `@observe` decorators on solution code paths | OTLP exporter | | **When active** | `LANGFUSE_HOST` configured | `OTEL_ENABLED=true` | See [LLM Observability](/deployment/observability/llm-observability) for a conceptual overview of how these complement each other. ## Auto-Enabled Integration Langfuse is auto-enabled when `LANGFUSE_HOST` is set in any solution's environment. The integration works through **LiteLLM callbacks** plus `@observe` decorators on key code paths. No code changes are needed in the solution services. When enabled, Langfuse captures: * Every LLM API call (model, prompt, completion, tokens, cost, latency) * Agent session traces (multi-turn conversation flows) * Evaluation results from scheduled and manual evaluations * Prompt template versions and their usage ## Cross-service tracing Recent solution updates introduced a unified tracing model so a single user conversation produces a single, end-to-end trace, even when it crosses agent boundaries: * **Auto-instrumented entry points.** Chat dispatch and the pipeline executor are decorated with `@observe`, so every conversation turn opens a trace span automatically. * **A2A trace context propagation.** When one agent calls another via the A2A protocol, the trace context propagates through the A2A metadata so the receiving agent's spans become children of the originating trace, not a new root. * **Deterministic trace IDs from `turn_id`.** Trace IDs are seeded from the conversation turn ID. Reprocessing the same turn (e.g., a retried evaluation) emits to the same trace rather than creating a duplicate. * **Per-iteration spans inside agent loops.** Multi-step agent loops emit one span per iteration so you can see how many planning steps a question took, how many tool calls each step made, and where time was spent. The net effect: the Langfuse UI shows one trace per user question, regardless of how many services or LLM calls were involved. ## Deployment Langfuse is self-hosted. Choose the deployment model that fits your infrastructure. For single-VM, demo, or local deployments, use the public upstream `langfuse/langfuse` Docker image: ```bash theme={null} # Get the upstream docker-compose.yml curl -L -o docker-compose.yml https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml # Configure environment (see langfuse.com/docs for full reference) # Required: NEXTAUTH_SECRET, SALT, database credentials docker compose up -d ``` This starts the standard Langfuse stack: Web (Next.js), Worker, PostgreSQL, ClickHouse, Redis, and a blob store. See [langfuse.com/docs/deployment/self-host](https://langfuse.com/docs/deployment/self-host) for the upstream self-hosting guide and configuration reference. For production Kubernetes deployments, use the upstream Helm chart: ```bash theme={null} helm repo add langfuse https://langfuse.github.io/langfuse-k8s helm install langfuse langfuse/langfuse \ --namespace langfuse \ --create-namespace \ --set langfuse.nextauth.secret="" \ --set langfuse.salt="" \ --set postgresql.auth.password="" ``` See [langfuse.com/docs/deployment/self-host/kubernetes](https://langfuse.com/docs/deployment/self-host/kubernetes) for full Helm values. ## Connecting CRAFT solutions to Langfuse After deploying Langfuse (upstream or internal fork), configure each solution to send traces. ### Data Insights (em-talk2data) ```yaml theme={null} # Helm values or environment em-talk2data: extraEnvVars: - name: LANGFUSE_HOST value: "https://langfuse.example.com" - name: LANGFUSE_PUBLIC_KEY valueFrom: secretKeyRef: name: langfuse-credentials key: public-key - name: LANGFUSE_SECRET_KEY valueFrom: secretKeyRef: name: langfuse-credentials key: secret-key ``` ### Data Governance (em-data-readiness) Same environment variables: `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`. ### Semiconductor (em-semi) Same environment variables applied to the Backend API and AI Agent services. ## Required Environment Variables | Variable | Description | | --------------------- | -------------------------------------------------------------------- | | `LANGFUSE_HOST` | URL of your Langfuse instance (e.g., `https://langfuse.example.com`) | | `LANGFUSE_PUBLIC_KEY` | Project public key from Langfuse dashboard | | `LANGFUSE_SECRET_KEY` | Project secret key from Langfuse dashboard | ## Accessing Langfuse After deployment, access the Langfuse UI at your configured host URL. 1. Create an account or log in. 2. Create a **project** for each solution you want to trace. 3. Copy the **Public Key** and **Secret Key** from the project settings. 4. Configure these in the solution's environment (see above). ## Related Why LLM observability matters and how Langfuse and OTel complement each other. Step-by-step setup including all configuration options. Infrastructure-level observability (metrics, traces, logs). # LLM Observability Source: https://docs.emergence.ai/deployment/observability/llm-observability Understanding LLM-specific observability for CRAFT, why it differs from infrastructure observability and how Langfuse and OpenTelemetry complement each other. # LLM Observability CRAFT uses two complementary observability systems that operate at different layers: * **OpenTelemetry**, infrastructure-level: HTTP requests, database queries, Redis, service health * **Langfuse**, LLM-level: model calls, token usage, cost, prompt quality, evaluation results Both are valuable; neither replaces the other. ## Why LLM Observability Is Different Traditional observability (metrics, traces, logs) was designed for deterministic systems. LLM-powered applications introduce unique observability challenges: | Challenge | Why it matters | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Non-determinism** | The same prompt can produce different outputs. Standard error rates don't capture quality degradation. | | **Token economics** | Cost is proportional to token usage, not request count. A single call can cost $0.001 or $1.00 depending on prompt length. | | **Prompt engineering** | Changing a prompt is a configuration change, not a code change, but it can dramatically affect output quality. | | **Evaluation** | "Is this response correct?" requires domain-specific evaluation, not just latency or error rate checks. | | **Multi-turn context** | A user session spans multiple LLM calls. Standard tracing doesn't capture the logical conversation flow. | ## What Each System Captures ### OpenTelemetry (Infrastructure) ```text theme={null} Service → [HTTP/DB/Redis spans] → OTel Collector → Grafana ``` | Signal | Examples | | ----------- | ----------------------------------------------------- | | **Metrics** | Request rate, P99 latency, error rate, pod CPU/memory | | **Traces** | End-to-end request flow through services | | **Logs** | Service logs, error messages, audit events | Answers: "Is the service up? Is it slow? Are there errors?" ### Langfuse (LLM) ```text theme={null} LLM call → [LiteLLM callback] → Langfuse ``` | Signal | Examples | | --------------- | ----------------------------------------------------------- | | **Traces** | Complete LLM session with all turns, tools, and context | | **Metrics** | Tokens used, estimated cost, latency per model call | | **Evaluations** | LLM-as-a-judge quality scores, human labels, rubric results | | **Prompts** | Version history, usage statistics, A/B comparison | Answers: "Is the AI producing quality output? What's it costing? Which prompt version is better?" ## Architecture ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB subgraph App["Application Services"] Svc["em-talk2data / em-semi"] LLM["LiteLLM (LLM proxy)"] end subgraph OTel["Infrastructure Observability"] OTelC["OTel Collector"] Prom["Metrics Backend"] Grafana["Grafana"] end subgraph LFuse["LLM Observability"] LF["Langfuse"] end Svc -->|HTTP spans| OTelC Svc --> LLM LLM -->|LiteLLM callbacks| LF LLM -->|LLM API calls| ExtLLM["Gemini / GPT / Claude"] OTelC --> Prom --> Grafana LF --> LFDash["Langfuse Dashboard"] classDef app fill:#E69F00,stroke:#555555,color:#000 classDef otel fill:#009E73,stroke:#555555,color:#000 classDef lf fill:#0072B2,stroke:#555555,color:#fff classDef ext fill:#CC79A7,stroke:#555555,color:#000 class Svc,LLM app class OTelC,Prom,Grafana otel class LF,LFDash lf class ExtLLM ext ``` ## Integration Pattern The platform uses **LiteLLM** as a provider-agnostic LLM proxy. LiteLLM natively supports Langfuse as a callback handler, requiring no changes to application code. When `LANGFUSE_HOST` is set, LiteLLM automatically: 1. Records each LLM API call to Langfuse (prompt, completion, model, tokens, cost) 2. Groups calls into sessions by conversation ID 3. Reports evaluation scores if evaluators are configured ## Langfuse Trace Anatomy A Langfuse trace for a Data Insights session might look like: ```text theme={null} Session: "user-query-schema-explain" └─ Trace: /api/chat (3.2s) ├─ Span: schema_lookup (0.3s) [DB query] ├─ Span: LLM call (2.5s) │ Model: gemini-3.5-flash │ Input tokens: 1,240 │ Output tokens: 380 │ Cost: $0.0018 │ Quality score: 0.87 (LLM-as-judge) └─ Span: format_response (0.4s) ``` ## Cost Tracking Langfuse aggregates LLM costs across all calls, enabling: * Cost per conversation / session / user * Cost trends over time * Model comparison (cost vs. quality tradeoff) * Budget alerting (configurable thresholds) ## Related Deploy and configure Langfuse. Infrastructure observability with OTel. # OpenTelemetry Source: https://docs.emergence.ai/deployment/observability/opentelemetry Configure OpenTelemetry for distributed tracing, metrics, and log export across all em-runtime services using the vendor-neutral OTLP protocol. # OpenTelemetry All em-runtime services export traces, metrics, and logs via [OpenTelemetry](https://opentelemetry.io/) using the vendor-neutral OTLP protocol. The platform ships with built-in instrumentation -- no application code changes are needed. ## Architecture ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB subgraph Apps["Applications (OTel Built-in)"] GOV["governance"] AST["assets"] UTL["utils"] T2D["Data Insights"] DRY["Data Governance"] KC["Keycloak (/metrics)"] end subgraph Collection["Collection"] OTel["OpenTelemetry Collector"] end subgraph Storage["Storage"] Prom["Metrics Backend"] Tempo["Grafana Tempo (Traces)"] Loki["Loki (Logs)"] end subgraph Viz["Visualization"] Grafana["Grafana"] AM["Alertmanager"] end GOV & AST & UTL & T2D & DRY -->|OTLP gRPC/HTTP| OTel KC -->|metrics scrape| Prom OTel -->|Remote Write| Prom OTel -->|OTLP| Tempo Prom & Tempo & Loki --> Grafana Prom --> AM classDef app fill:#E69F00,stroke:#555555,color:#000 classDef collect fill:#56B4E9,stroke:#555555,color:#000 classDef storage fill:#009E73,stroke:#555555,color:#000 classDef viz fill:#0072B2,stroke:#555555,color:#fff class GOV,AST,UTL,T2D,DRY,KC app class OTel collect class Prom,Tempo,Loki storage class Grafana,AM viz ``` The OTLP collector is **not** included in the em-runtime Helm chart. Deploy your preferred collector separately. Any OTLP-compatible backend works: Grafana Cloud, Datadog, Splunk, New Relic, Honeycomb, or self-hosted. ## OpenTelemetry vs Langfuse These two observability systems are **complementary**, not overlapping: | | OpenTelemetry | Langfuse | | ----------------- | ------------------------------------------------ | ----------------------------------------------------- | | **Instruments** | HTTP requests, DB queries, Redis, service health | LLM API calls (model, prompt, tokens, cost, quality) | | **Answers** | "Is the service healthy? Is it slow?" | "Is the AI producing good output? What does it cost?" | | **Integration** | OTLP exporter (always active) | LiteLLM callbacks (when `LANGFUSE_HOST` is set) | | **Storage** | PromQL-compatible metrics backend / Tempo / Loki | Langfuse's own PostgreSQL + ClickHouse | | **Visualization** | Grafana | Langfuse UI | Use both together: OTel for infrastructure health, Langfuse for LLM quality and cost. See the Langfuse setup guide for LLM-specific observability. ## Configuration Telemetry is controlled via environment variables in each service's `env` block in the Helm values: | Variable | Default | Description | | ----------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------- | | `OTEL_ENABLED` | `"true"` | Master switch for all telemetry. | | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://otel-collector:4317"` | OTLP collector gRPC endpoint. | | `OTEL_TRACES_ENABLED` | `"true"` | Enable distributed tracing. | | `OTEL_METRICS_ENABLED` | `"true"` | Enable metrics export. | | `OTEL_LOGS_ENABLED` | `"true"` | Enable log record export via OTLP. | | `OTEL_TRACE_SAMPLE_RATE` | `"1.0"` | Trace sampling ratio (0.0-1.0). Default is 100%; override via Helm values for production. | ### Point to Your Collector Override the endpoint in your values file: ```yaml theme={null} em-runtime-governance: env: OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring:4317" em-runtime-assets: env: OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring:4317" em-runtime-utils: env: OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring:4317" ``` ### Disable Telemetry For test environments without a collector: ```yaml theme={null} em-runtime-governance: env: OTEL_ENABLED: "false" ``` Individual signals can also be toggled independently. Telemetry initialization is non-fatal. If the collector is unreachable, services log a warning and continue operating normally. ## Auto-Instrumented Libraries The following libraries are automatically instrumented with no code changes: | Library | What It Captures | | -------------- | ----------------------------------------------------- | | **FastAPI** | Inbound HTTP request spans (excludes `/health`) | | **SQLAlchemy** | Database query spans and connection metrics | | **Redis** | Redis command spans | | **httpx** | Outbound HTTP request spans (inter-service SDK calls) | ## Telemetry Signals ### Traces Distributed traces follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) format. Traces propagate across service boundaries automatically via httpx instrumentation. Key trace fields: * `service.name` -- identifies the emitting service * `http.method`, `http.url`, `http.status_code` -- HTTP span attributes * `db.system`, `db.statement` -- database query details * `trace_id` -- correlates logs and traces ### Metrics Application metrics are exported via OTLP and include: * HTTP request duration histograms * HTTP request counts by status code * Database connection pool utilization * Redis command latency ### Logs Structured JSON logs are written to stdout and optionally exported via OTLP: | Field | Description | | ----------- | --------------------------------------- | | `level` | Log level (DEBUG, INFO, WARNING, ERROR) | | `message` | Log message | | `timestamp` | ISO 8601 timestamp | | `service` | Service name | | `trace_id` | W3C trace ID for log-trace correlation | ## OTel Collector Configuration Deploy the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) to receive, process, and export telemetry data. ### Recommended Setup ```yaml theme={null} # otel-collector-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 1024 exporters: # Traces otlp/tempo: endpoint: tempo.monitoring:4317 tls: insecure: true # Metrics prometheusremotewrite: endpoint: http://prometheus.monitoring:9090/api/v1/write # Logs loki: endpoint: http://loki.monitoring:3100/loki/api/v1/push service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/tempo] metrics: receivers: [otlp] processors: [batch] exporters: [prometheusremotewrite] logs: receivers: [otlp] processors: [batch] exporters: [loki] ``` ### Helm Installation ```bash theme={null} helm install otel-collector open-telemetry/opentelemetry-collector \ --namespace monitoring \ --create-namespace \ -f otel-collector-config.yaml ``` ## Metric Sources | Source | Endpoint | Protocol | | --------------------------- | --------------------------------- | ---------------------------- | | Application services (OTel) | OTLP gRPC (4317) / HTTP (4318) | OpenTelemetry | | Keycloak | `/keycloak/metrics` (port 8080) | Prometheus exposition format | | Kubernetes | kube-state-metrics, node-exporter | Prometheus exposition format | | PostgreSQL | pg\_exporter (optional) | Prometheus exposition format | | Redis | redis\_exporter (optional) | Prometheus exposition format | ## Backend Options | Component | Purpose | | ----------------- | ------------------------------ | | **Prometheus** | Metrics storage and querying | | **Grafana Tempo** | Distributed trace storage | | **Loki** | Log aggregation | | **Grafana** | Dashboards and visualization | | **Alertmanager** | Alert routing and notification | Deploy via the [Grafana LGTM Helm chart](https://grafana.com/docs/lgtm/latest/get-started/install/kubernetes/) or individual component charts. Point the OTel Collector exporters to Grafana Cloud endpoints: ```yaml theme={null} exporters: otlp/grafana: endpoint: tempo-.grafana.net:443 headers: Authorization: "Basic " ``` Use the Datadog Agent as an OTLP receiver: ```yaml theme={null} exporters: datadog: api: key: ${DD_API_KEY} ``` Use the AWS Distro for OpenTelemetry (ADOT) Collector: ```yaml theme={null} exporters: awsxray: {} awsemf: {} ``` ## Next Steps Telemetry environment variables in Helm values. Infrastructure requirements for the observability stack. # CRAFT Deployment Overview Source: https://docs.emergence.ai/deployment/overview Deploy CRAFT on any Kubernetes cluster using Helm charts and ArgoCD GitOps. Supports GKE, EKS, AKS, and on-premises environments. # Deployment Overview Run CRAFT on any Kubernetes cluster. The platform uses **Helm charts** for application packaging and **ArgoCD** for GitOps-based continuous deployment across four environments: dev, staging, demo, and production. CRAFT deploys on **your infrastructure**. The platform runs on any CNCF-conformant Kubernetes cluster -- GKE, EKS, AKS, or self-managed -- without cloud-specific dependencies. You choose where to run; the same Helm chart deploys across all environments, while provisioning steps vary by environment. ## Supported Environments Deploy on GKE Autopilot or Standard clusters. Terraform modules automate cluster, VPC, IAM, and Secret Manager provisioning. Deploy on Amazon EKS with Helm charts. Terraform modules for automated provisioning are planned. Deploy on Azure AKS with Helm charts. Terraform modules for automated provisioning are planned. Deploy on any CNCF-conformant cluster (v1.28+). No cloud-specific services required -- use self-hosted PostgreSQL, Redis, and MinIO. ## Helm Chart The em-runtime Helm chart packages all platform services and their dependencies: ### Included Components | Component | Type | Purpose | | -------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | **Governance** | Deployment | Identity, authorization, secrets management (port 8000) | | **Assets** | Deployment | Artifacts, data connections, files, models (port 8000) | | **Utils** | Deployment | Data catalog, search, scheduling, context packs, memories (port 8000) | | **PostgreSQL** | StatefulSet / External | Primary data store for all services | | **Redis** | StatefulSet / External | Caching, messaging, event streaming | | **Keycloak** | StatefulSet / External | Multi-realm JWT authentication | | **OpenFGA** | Deployment / External | Relationship-based access control | | **Infisical** | Deployment / External | Secrets management, one of two supported backends. See [Secrets Management](/security/secrets-management). | ### Deployment Modes All dependencies (PostgreSQL, Redis, Keycloak, OpenFGA) are deployed as part of the Helm release within the Kubernetes cluster. Best for: * Development and testing environments * Standalone deployments * Quick setup with minimal external dependencies PostgreSQL and Redis run outside the Kubernetes cluster (managed services or Docker). The Helm chart connects to them via configuration values. Best for: * Production environments with managed database services * Existing infrastructure reuse * Separation of stateful and stateless workloads ### Solution Integration Solutions declare `em-runtime` as a **Helm subchart dependency** in their `Chart.yaml`. This ensures: * Platform services are co-deployed with the solution * Consistent Keycloak, OpenFGA, and database configuration * Single `helm install` deploys the complete stack * Version-pinned dependencies for reproducible deployments ## Service Startup Order The Helm chart respects the required startup order via init containers and readiness probes: ```text theme={null} PostgreSQL -> Redis -> Keycloak -> OpenFGA -> [Infisical] -> Governance -> Assets/Utils -> Solutions ``` Governance must complete its bootstrap process (initializing Keycloak realms and OpenFGA schema) before Assets, Utils, or any solution can start. If Governance is down, downstream services return 403 on all permission-checked endpoints. ## Observability Stack The deployment includes a full observability stack via the Grafana LGTM bundle: | Component | Purpose | | ----------------- | ------------------------------------------------------ | | **OpenTelemetry** | OTLP-based traces, metrics, and logs from all services | | **Loki** | Log aggregation and querying | | **Grafana** | Dashboards and visualization | | **Tempo** | Distributed tracing | | **Mimir** | Metrics storage and querying | ## Next Steps Customize the Helm chart values for your environment. Set up OpenTelemetry and the Grafana LGTM stack. Gateway API, cert-manager, and DNS configuration. Configure ESO + GCP Secret Manager or Infisical. # Prerequisites Source: https://docs.emergence.ai/deployment/prerequisites Infrastructure, networking, database, and access requirements for deploying CRAFT on Kubernetes. # Prerequisites Before deploying EM-Runtime, ensure your environment meets the infrastructure, database, networking, and access requirements listed below. ## Kubernetes Cluster **1.28+** required for Gateway API v1 support. **3 nodes** for HA pod distribution across failure domains. **4 vCPU, 16 GB RAM** per node. **8 vCPU, 32 GB RAM** per node for HPA burst headroom. | Requirement | Specification | | ------------------------- | -------------------------------------------------------------------------------- | | **Cluster type** | Managed (GKE, EKS, AKS) or self-managed (RKE2, k3s, OpenShift) | | **CNI plugin** | Calico, Cilium, or cloud-native CNI | | **Gateway API** | Gateway API v1 controller (NGINX Gateway Fabric, Envoy Gateway, or cloud-native) | | **cert-manager** | v1.x for TLS certificate lifecycle | | **external-dns** | Optional, for automatic DNS record creation | | **Image registry access** | Pull access to `ghcr.io/emergenceai/*` (private) | ### IP Address Planning Allocate three non-overlapping CIDR ranges: | Range | Purpose | Recommended Size | | ------------ | ------------------- | --------------------- | | Node CIDR | Kubernetes node IPs | /20 (4,096 addresses) | | Pod CIDR | Pod network | /19 (8,192 addresses) | | Service CIDR | ClusterIP services | /22 (1,024 addresses) | ## Compute Resources ### Per-Component Resource Allocation The table shows **Helm chart defaults** (suitable for dev/staging) and **production overrides** (used in GCP deployments). | Component | Min Replicas | CPU Request (Default / Prod) | CPU Limit | Memory Request (Default / Prod) | Memory Limit | | | --------------------- | ------------- | ---------------------------- | --------- | ------------------------------- | ------------ | ------------------------------------------ | | Keycloak | 2 | 1000m / 1000m | 2000m | 1280Mi / 1536Mi | 2Gi | | | OpenFGA | 2 (HPA: 2-10) | 100m / 200m | 500m | 256Mi / 256Mi | 512Mi | | | Infisical | 2 | 350m / 500m | 1000m | 512Mi / 512Mi | 1Gi | (if using Infisical backend; omit for ESO) | | em-runtime-governance | 2 (HPA: 2-10) | 250m / 500m | 1000m | 512Mi / 512Mi | 1Gi | | | em-runtime-assets | 2 (HPA: 2-10) | 250m / 500m | 1000m | 512Mi / 512Mi | 1Gi | | | em-runtime-utils | 2 (HPA: 2-10) | 100m / 250m | 500m | 256Mi / 256Mi | 512Mi | | ### Aggregate Totals (Core Only, Helm Defaults) | Scenario | CPU Request | CPU Limit | Memory Request | Memory Limit | | ------------------------------------ | ----------- | --------- | -------------- | ------------ | | **Minimum (all at min replicas)** | 4.1 vCPU | 12.0 vCPU | 6.5 GiB | 12.0 GiB | | **Maximum (HPA services at max 10)** | 9.7 vCPU | 36.0 vCPU | 18.5 GiB | 36.0 GiB | Add 10-20% overhead for system pods (kube-system, cert-manager, external-dns, gateway controller, monitoring agents). Companion solution services (Data Insights, Data Governance) add additional resources. ## Infrastructure Overview ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB subgraph Cluster["Kubernetes Cluster (3+ nodes, 4vCPU/16GB each)"] GW["Gateway API v1\n(HTTPRoute + TLS)"] subgraph Services["Platform Services"] KC["Keycloak 26\n(2 replicas)"] OF["OpenFGA 1.11\n(2–10 replicas)"] GV["Governance\n(2–10 replicas)"] AS["Assets\n(2–10 replicas)"] UT["Utils\n(2–10 replicas)"] end end subgraph Data["Data Layer"] PG[("PostgreSQL 18+\n9 databases\n2vCPU / 4GB / 100GB SSD")] RD[("Redis 8.4.2\n5GB / noeviction")] end OBJ[("Object Storage\n(S3-compatible, private)")] CERT["cert-manager\n(TLS certificates)"] GW --> Services Services --> PG & RD Services --> OBJ CERT --> GW classDef cluster fill:#56B4E9,stroke:#555555,color:#000 classDef svc fill:#E69F00,stroke:#555555,color:#000 classDef data fill:#0072B2,stroke:#555555,color:#fff classDef ext fill:#009E73,stroke:#555555,color:#000 class GW cluster class KC,OF,GV,AS,UT svc class PG,RD data class OBJ,CERT ext ``` ## Database Requirements ### PostgreSQL | Parameter | Specification | | ----------------------- | ------------------------------- | | **Version** | PostgreSQL 18 | | **Instance sizing** | 2 vCPU, 4 GB RAM (minimum) | | **High availability** | Multi-AZ / regional replication | | **Initial disk** | 100 GB SSD | | **Maximum disk** | 1,000 GB (auto-resize) | | **Backup retention** | 7 days minimum | | **Required extensions** | `pg_cron` (on `utils` database) | #### Databases (9 total, single shared instance) | Database | Used By | | --------------- | ---------------------------------------------------------------------------------- | | `keycloak` | Keycloak IAM | | `openfga` | OpenFGA authorization | | `infisical` | Infisical secrets management (provisioned by default; unused if using ESO backend) | | `governance` | Platform Governance service | | `assets` | Platform Assets service | | `utils` | Platform Utilities service (+ pg\_cron) | | `prefect` | Prefect workflow orchestration (Data Governance) | | `datareadiness` | Data quality and metadata (Data Governance) | | `talk2data` | Natural language query engine (Data Insights) | ### Redis | Parameter | Specification | | --------------------- | ---------------------------------------------------------- | | **Version** | Redis 8.4.2 (self-managed) / Redis 7.0+ (managed services) | | **Memory** | 5 GB | | **High availability** | Multi-AZ replica (STANDARD\_HA) | | **Authentication** | Password-protected (AUTH required) | | **TLS** | Supported (`rediss://` scheme) | | **Eviction policy** | `noeviction` (reject writes when full) | ### Object Storage (S3-Compatible) | Parameter | Specification | | ------------------- | ------------------------------------------- | | **Protocol** | S3-compatible API (AWS S3, GCS HMAC, MinIO) | | **Access control** | Private (no public access) | | **Authentication** | Access key + secret key (HMAC) | | **Bucket name** | Configurable (default: `em-runtime`) | | **Max file upload** | 1,000 MB (configurable) | ## Networking ### Service Ports (Internal) | Service | Port | Protocol | | | --------------------- | ---- | -------------------------- | ---------------------------- | | em-runtime-governance | 8000 | HTTP | | | em-runtime-assets | 8000 | HTTP | | | em-runtime-utils | 8000 | HTTP | | | Keycloak | 8080 | HTTP | | | OpenFGA | 8080 | HTTP + gRPC (health: 8081) | | | Infisical | 8080 | HTTP | (if using Infisical backend) | | PostgreSQL | 5432 | TCP | | | Redis | 6379 | TCP | | ### External Ingress (HTTPRoute Rules) | Path | Backend | Port | Notes | | ----------------- | --------------------- | ---- | ------------------------- | | `/keycloak` | keycloak | 8080 | Pass-through (no rewrite) | | `/api/governance` | em-runtime-governance | 8000 | Rewrite to `/governance` | | `/api/assets` | em-runtime-assets | 8000 | Rewrite to `/assets` | | `/api/utils` | em-runtime-utils | 8000 | Rewrite to `/utils` | ### TLS Requirements | Boundary | Requirement | | ----------------------- | ---------------------------------- | | Client to load balancer | TLS 1.2+ (certificate required) | | Load balancer to pods | HTTP (in-cluster; TLS optional) | | Services to PostgreSQL | SSL recommended (private network) | | Services to Redis | TLS supported (`rediss://` scheme) | ### Firewall Rules Ensure the following traffic is allowed: * **Inbound:** HTTPS (443) * **From cluster:** PostgreSQL (5432), Redis (6379), S3 (443) * **LLM API egress:** OpenAI, Anthropic, Vertex AI endpoints (for Data Insights and Data Governance) ## Required Secrets Create these credentials before deployment: | Secret | Description | | ------------------------ | --------------------------------------- | | `POSTGRES_PASSWORD` | PostgreSQL superuser password | | `POSTGRES_HOST` | PostgreSQL hostname or IP | | `REDIS_PASSWORD` | Redis AUTH password | | `REDIS_HOST` | Redis hostname or IP | | `S3_ACCESS_KEY_ID` | Object storage access key | | `S3_SECRET_ACCESS_KEY` | Object storage secret key | | `S3_ENDPOINT_URL` | Object storage endpoint | | `S3_BUCKET_NAME` | Bucket name | | OCI registry credentials | Pull secret for `ghcr.io/emergenceai/*` | ## Required Tools | Tool | Version | Purpose | | --------- | ------- | -------------------------------------- | | `kubectl` | Latest | Kubernetes cluster management | | `helm` | 3.x | Helm chart installation | | `gh` | Latest | GitHub CLI for registry authentication | ```bash theme={null} # Authenticate GitHub CLI with required scopes gh auth login --scopes read:packages,repo,workflow ``` ## Pre-Deployment Checklist * Kubernetes cluster provisioned (1.28+, 3+ nodes, 4+ vCPU / 16+ GB RAM each) * Gateway API controller installed and operational * cert-manager installed * kubectl access configured with cluster-admin or equivalent * PostgreSQL 18 instance provisioned with multi-AZ * 9 databases created * `pg_cron` extension installed on `utils` database * Redis instance provisioned (5 GB, AUTH enabled, `noeviction` policy) * S3-compatible bucket created (private access) * HMAC access key and secret key provisioned * DNS record planned for platform hostname * TLS certificate available * All database and storage credentials available * Container registry credentials for `ghcr.io/emergenceai/*` * Helm 3.x and `gh` CLI installed and authenticated * Access to `oci://ghcr.io/emergenceai/charts` verified ## Next Steps Configure the Helm chart for your environment. # CRAFT Architecture Source: https://docs.emergence.ai/getting-started/architecture The 3-layer architecture of CRAFT: Solutions, Platform, and Infrastructure layers with repo mapping and dependency model. # Architecture CRAFT uses a **3-layer architecture**. Your domain-specific **Solutions** build on a shared **Platform**, which runs on managed **Infrastructure**. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD subgraph Solutions["Solutions Layer"] T2D["Data Insights"] DR["Data Governance"] SEMI["Semiconductor"] end subgraph Platform["Platform Layer (em-runtime)"] GOV["Governance
/api/governance
Auth, Orgs, Permissions"] ASS["Assets
/api/assets
Data Connections, Models"] UTL["Utils
/api/utils
Catalog, Scheduling, Memories"] end subgraph Infra["Infrastructure Layer"] K8S["Kubernetes"] PG["PostgreSQL"] RED["Redis"] KC["Keycloak"] FGA["OpenFGA"] end T2D --> GOV T2D --> ASS DR --> GOV DR --> ASS SEMI --> GOV GOV --> PG GOV --> KC GOV --> FGA ASS --> PG ASS --> RED UTL --> PG UTL --> RED style Solutions fill:#E69F00,stroke:#555555,color:#000 style Platform fill:#56B4E9,stroke:#555555,color:#000 style Infra fill:#0072B2,stroke:#555555,color:#fff ``` ## The Three Layers Domain-specific applications that solve customer problems. Each solution is a standalone product with its own data model, agents, and UI, but shares platform services for identity, authorization, secrets, and resource management. | Solution | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Data Insights** | Multi-agent conversational data analytics using the A2A protocol. Users ask natural-language questions; agents generate SQL, execute queries, analyze results, and produce visualizations. | | **Data Governance** | Metadata enrichment, data profiling, DQ rule generation, and scorecard reporting. Orchestrated by Prefect workflows with LLM-powered enrichment. | | **Semiconductor** | Semiconductor fab analytics with AI-powered analysis agents and context graph. | Shared services consumed by all solutions. Provides identity, authorization, resource management, and a unified UI shell. | Service | Port | Responsibility | | -------------- | ---- | ------------------------------------------------------------------------------------------- | | **Governance** | 8000 | Organizations, projects, users, Keycloak multi-realm auth, OpenFGA ReBAC, Secrets API | | **Assets** | 8000 | Artifacts, data connections, files, models | | **Utils** | 8000 | Data catalog, search, scheduling, context packs, memories, operational utilities | | **Runtime UI** | -- | React microfrontend shell using Module Federation 2.0; hosts solution UIs as remote modules | All platform services share a common auth flow: solutions forward user JWT tokens to Governance for permission validation via auto-generated SDKs. Cloud deployment, networking, and GitOps automation. | Component | Tool | Responsibility | | ------------------------------- | --------- | --------------------------------------------------------------------- | | **Infrastructure Provisioning** | Terraform | Kubernetes clusters, networking, IAM, secrets backend | | **GitOps Delivery** | ArgoCD | Continuous deployment from Helm charts to 4 environments | | **Helm Charts** | Helm | Application chart templates with environment-specific values overlays | ## How Solutions Depend on the Platform Solutions consume platform services through two integration mechanisms: ### 1. Helm Subchart Dependency Each solution's Helm chart declares `em-runtime` as a subchart dependency. At deploy time, the platform services (Keycloak, OpenFGA, PostgreSQL, Redis) are co-deployed or connected via shared infrastructure values. This ensures every solution gets consistent identity, authorization, and secrets management without duplicating infrastructure. ### 2. SDK-Based Runtime Integration em-runtime auto-generates Python and TypeScript SDKs from its OpenAPI specs. Solutions import these SDKs to: * **Authenticate users** -- forward JWT tokens to Governance for permission validation * **Manage resources** -- CRUD operations on agents, artifacts, data connections via the Assets SDK * **Check permissions** -- OpenFGA relationship-based access control via the Governance SDK Solutions never implement their own permission checks. They delegate to the platform via SDK calls. If Governance is down, downstream services return 403. ## Open Standards and Portable Components CRAFT is built on open standards and portable, open-source components at every layer: ### Standards and Protocols | Layer | Standard | Purpose | | ----------------------- | ------------------------ | ---------------------------------------------------------------------- | | **Agent Communication** | A2A (JSON-RPC 2.0 / SSE) | Open protocol for inter-agent messaging | | **Tool Integration** | MCP | Open protocol for connecting agents to external tools and data sources | | **Identity** | OIDC / PKCE | Standards-based authentication | | **Observability** | OTLP | Vendor-neutral telemetry export to any compatible backend | ### Open-Source Components | Layer | Component | Purpose | | ------------------ | --------- | ------------------------------------------------------------------------------------ | | **Authorization** | OpenFGA | Relationship-based access control (open-source engine) | | **Object Storage** | obstore | Portable storage across GCS, Azure Blob, S3-compatible stores, and local filesystems | | **LLM Access** | LiteLLM | Provider-agnostic gateway -- use Gemini, GPT, Claude, or any supported model | Every protocol and component listed above is an open standard or open-source project. The platform does not require any proprietary service -- you can deploy on any infrastructure that supports these standards. ## Next Steps Learn about the core entities: organizations, projects, agents, and data connections. Deep dive into the em-runtime services that power the platform layer. Explore the multi-agent data analytics solution. Learn how to deploy the platform on Kubernetes. # CRAFT Core Concepts Source: https://docs.emergence.ai/getting-started/concepts Core concepts of CRAFT: organizations, projects, agents, data connections, schedules, and multi-tenancy. # Key Concepts This page introduces the foundational concepts you need to work with CRAFT. Understanding these concepts helps you build and operate any solution on the platform. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% erDiagram Organization ||--o{ Project : "contains" Organization ||--o{ User : "has members" Project ||--o{ Agent : "registers" Project ||--o{ DataConnection : "configures" Project ||--o{ Schedule : "manages" Project ||--o{ ContextPack : "contains" Organization { string org_id PK string name string keycloak_realm } Project { string project_id PK string org_id FK string name } Agent { string agent_id PK string project_id FK string protocol } DataConnection { string connection_id PK string project_id FK string type } ``` ## Core Entities An **organization** is the top-level tenant boundary in CRAFT. Every resource in the platform belongs to exactly one organization. **Key characteristics:** * Each organization maps to a single **Keycloak realm** (the realm ID equals the org ID) * Organizations are created during the Governance service bootstrap process * All users, projects, and resources are scoped to an organization * Cross-organization data access is prevented at the database query level **Identity flow:** When you authenticate, your JWT token contains the `org_id` derived from the Keycloak realm. All subsequent API calls are scoped to that organization automatically. **Related:** [Organizations](/platform/organizations) | [Authentication](/platform/authentication) A **project** is a logical grouping of resources within an organization. Projects enable teams to organize agents, data connections, and other assets into separate workspaces. **Key characteristics:** * Projects belong to an organization and inherit its permission structure * The `project_id` is passed via the `X-Project-ID` HTTP header or as a path parameter -- it is never stored in the JWT * Users can have different roles across different projects within the same organization * All list queries filter by both `org_id` (from JWT) and `project_id` (from header) **Permission inheritance:** Projects inherit permissions from their parent organization via OpenFGA. A user who is an `admin` of the organization automatically has admin-level computed permissions on all projects within it. **Related:** [Projects](/platform/projects) | [Authorization](/platform/authorization) | [Authenticate Users (for solution developers)](/guides/solution-dev/authenticate-users) An **agent** is a registered AI service that can be discovered, invoked, and monitored through the platform. CRAFT supports three agent ecosystems through a unified registry: | Protocol | Description | Discovery | | -------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------ | | **A2A** (Agent-to-Agent) | Open protocol for inter-agent communication using JSON-RPC 2.0 over SSE | Agent Cards at `/.well-known/agent-card.json` | | **MCP** (Model Context Protocol) | Protocol for providing context to LLMs through tool definitions and resource access | MCP server metadata with tool/resource/prompt indexing | | **agentskills.io** | Portable skill format for packaging agent capabilities | Skill packages linked to Agent Card registrations | Agents are registered in the **Assets** service (port 8000) and can be searched across all three protocols with a single query. **Agent lifecycle stages:** EXPERIMENTAL, STABLE, DEPRECATED, RETIRED **Related:** [Agent Registry](/platform/agents) | [Data Insights Agents](/data-insights/overview) A **data connection** is a configured link to an external data source registered in the platform. Solutions use data connections to access customer data without managing credentials directly. **Key characteristics:** * Registered in the Assets service (port 8000) * Support databases, APIs, and file stores as data sources * Credentials are managed through the platform Secrets API (Infisical or ESO + GCP Secret Manager) * Scoped to an organization and project for multi-tenant isolation * Used by Data Insights for querying customer databases * Used by Data Governance for profiling and enrichment **Security:** Data connection credentials are never exposed in API responses. They are stored via the platform Secrets API and injected at runtime only when a solution needs to establish a connection. **Related:** [Data Connections](/platform/data-connections) | [Data Source Setup Guide](/guides/data-source-setup) A **schedule** defines recurring execution of tasks such as data profiling runs, agent health checks, or report generation. Schedules are managed by the **Utils** service (port 8000). **Key characteristics:** * Cron-based scheduling with timezone support * Scoped to an organization and project * Integrated with the platform's permission system (requires `can_execute` permission) * Execution history and status tracking **Related:** [Schedules](/platform/schedules) **Webhooks are planned for an upcoming release.** The webhook system will provide HTTP callbacks to notify external systems when platform events occur. Planned capabilities include HMAC-signed payloads, configurable event filters, automatic retry with exponential backoff, and SSRF protection. **Related:** [Webhooks (Planned)](/platform/webhooks) Multi-tenancy in CRAFT provides complete isolation between organizations while allowing flexible access control within each organization. **Isolation model:** * **Authentication isolation:** Each organization is a separate Keycloak realm with its own user directory, identity providers, and SSO configuration * **Authorization isolation:** OpenFGA stores per-organization relationship tuples; cross-organization access is structurally impossible * **Data isolation:** Each service owns its own database; all queries filter by `org_id` from the JWT token * **Secrets isolation:** Secret access is scoped per organization (Infisical namespaces or GCP SM IAM bindings) **Security invariants:** * The `org_id` always comes from the authenticated JWT, never from request parameters * List operations always filter by `org_id` AND `project_id` at the database level * Write operations always stamp the `org_id` from the JWT onto new resources * No cross-service foreign keys exist between databases **Related:** [Multi-Tenancy](/platform/multi-tenancy) | [Security](/security/overview) ## Permission Model The platform uses **Relationship-Based Access Control (ReBAC)** via OpenFGA. Permissions are computed from relationships rather than statically assigned. ### Roles | Role | Description | | ----------- | ------------------------------------------------------------------- | | `owner` | Full control over the organization and all its resources | | `admin` | Administrative access to manage users, projects, and settings | | `member` | Standard access to project resources | | `developer` | Access to create and modify agents, data connections, and workflows | | `operator` | Access to deploy, schedule, and monitor resources | | `viewer` | Read-only access to all resources | ### Computed Permissions Permissions are derived from role relationships at query time: | Permission | Description | | -------------------- | ------------------------------------------------------- | | `can_read` | View a resource and its metadata | | `can_write` | Create or update a resource | | `can_delete` | Remove a resource | | `can_execute` | Run an agent, trigger a schedule, or execute a workflow | | `can_manage_secrets` | Access and manage secrets for data connections | ### Inheritance Permissions flow downward through the hierarchy: ```text theme={null} Organization -> Project -> Resource (Agent, Data Connection, etc.) ``` A user with the `admin` role on an organization automatically receives `can_read`, `can_write`, and `can_delete` computed permissions on all projects and resources within that organization. ## CRAFT Modules CRAFT is one product, composed of three modules. Module names are stable nomenclature; capitalize as shown. | Module | Status | Purpose | | ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CRAFT Assess** | Available | Evaluates data and surfaces what blocks agent-readiness. Implemented today by the Data Insights and Data Governance solutions (data profiling, coverage analysis, policy compliance checks). | | **CRAFT Enrich** | Available | Automatically enriches metadata, generates data quality rules, and classifies data assets. Implemented today by the Data Governance solution (LLM-powered metadata enrichment, DQ rule generation, classification). | | **CRAFT Toolkit** | Planned | Verification certificates and auto-formalization tools for developers extending the platform. Custom data connectors are not part of Toolkit; they are added on-demand to the platform's data-connections capability. | See [Introduction](/getting-started/introduction#how-craft-is-composed) for the user-facing framing. ## Glossary of Key Terms | Term | Definition | | -------------- | --------------------------------------------------------------------------------------------------- | | **A2A** | Agent-to-Agent. Open protocol for inter-agent communication (JSON-RPC 2.0 over SSE). | | **Agent Card** | A JSON manifest at `/.well-known/agent-card.json` describing an A2A agent's capabilities and skills | | **MCP** | Model Context Protocol -- a protocol for providing context to LLMs through tool definitions | | **ReBAC** | Relationship-Based Access Control -- fine-grained authorization based on entity relationships | | **OIDC** | OpenID Connect -- the identity layer used by Keycloak for authentication | | **PKCE** | Proof Key for Code Exchange -- OAuth 2.0 extension for secure browser-based auth flows | | **OTel** | OpenTelemetry -- vendor-neutral observability framework for traces, metrics, and logs | | **LGTM** | Loki, Grafana, Tempo, Mimir -- the Grafana observability stack | | **SSE** | Server-Sent Events -- unidirectional HTTP streaming used by the A2A protocol | ## Next Steps Explore the platform services that implement these concepts. Deep dive into OpenFGA and the ReBAC permission model. Learn about the unified agent registry across A2A, MCP, and agentskills.io. Understand the full security architecture including authentication, authorization, and secrets. # Introduction to CRAFT Source: https://docs.emergence.ai/getting-started/introduction What CRAFT is, the three modules that compose it, and how the platform fits into your stack. # Welcome to CRAFT CRAFT is an enterprise intelligence platform. Agents reason across your enterprise data to drive day-to-day operational decisions and long-horizon strategic ones, inside a verified envelope of constraints, policies, and proofs. What that looks like in practice: root-cause analysis that traces problems to their source, anomaly detection that catches what humans miss, scenario modeling that stress-tests decisions before they're made, and Verified Talk-to-Data that lets anyone in your organization query enterprise data in plain English and trust the answer. CRAFT deploys on **your infrastructure**, in your cloud, in your data center, or both. Every component runs on any CNCF-conformant Kubernetes cluster without cloud-specific dependencies. You retain full control over your data, policies, and execution. ## How CRAFT is composed CRAFT is one product, composed of three modules that target different stages of the path from raw data to verified action. Evaluates your data and surfaces what is blocking agent-readiness. Identifies gaps in quality, coverage, and policy compliance before autonomous agents are deployed. Automatically enriches metadata, generates data quality rules, and classifies data assets so agents can reason on them reliably. Planned. Verification certificates and auto-formalization tools for developers extending the platform. Custom data connectors are not part of Toolkit; they live in CRAFT data connections and are added on-demand. Built on Emergence's neuro-formal approach: mathematical proof is embedded into the architecture, so bounded autonomy is structural by design rather than a layered-on feature. ## What is CRAFT (technically)? Under the modules, CRAFT is a **3-layer architecture**: 1. **Solutions** are domain-specific applications (Data Insights, Data Governance, Semiconductor Analytics) that solve customer problems using AI agents, LLMs, and workflow orchestration. 2. **Platform** provides shared services for identity, authorization, secrets management, asset management, and a unified UI shell. Every solution inherits these capabilities automatically. 3. **Infrastructure** provides Kubernetes-based provisioning, GitOps delivery, and Helm-based deployment across four environments (dev, staging, demo, prod). The two views are orthogonal: Assess and Enrich are *what* you do with CRAFT; the 3-layer architecture is *how* CRAFT is built. ## Who is it for? Get AI-powered insights from your enterprise data without writing code. Ask questions in natural language, explore dashboards, and make data-driven decisions faster. Query your organization's data through conversational AI. Generate reports, discover trends, and share findings without technical skills. Ask natural-language questions against your databases and get instant SQL-generated answers, visualizations, and insights through multi-agent orchestration. Profile data assets, enrich metadata with LLMs, generate data quality rules, and produce scorecards through automated workflows. Deploy and operate the platform on any Kubernetes cluster with Helm charts, ArgoCD GitOps, and full observability via OpenTelemetry and the Grafana LGTM stack. ## Key Capabilities * **Multi-agent orchestration** using the A2A protocol (JSON-RPC 2.0 over SSE) * **Asset Management** for data connections, artifacts, files, and models * **Pipeline framework** for stateful, multi-step agent workflows with cooperative cancellation * **Provider-agnostic LLM integration** via LiteLLM (Gemini, GPT, Claude, and more) * **Verified Talk-to-Data** with schema-aware natural-language to SQL, validation, and execution * **Data profiling and enrichment** powered by LLMs and orchestrated by Prefect workflows * **Data quality rules and scorecards** with automated generation and tracking * **Conversational analytics** with multi-turn context and visualization support * **Multi-tenant authentication** with OIDC/PKCE and single sign-on support * **Fine-grained authorization** with computed permissions across organizations and projects * **Secrets management** for cloud and self-hosted deployments. See [Secrets Management](/security/secrets-management) * **Multi-tenancy** with organization and project-level isolation * **Helm charts** with subchart dependency model for consistent deployments * **ArgoCD** GitOps across dev, staging, demo, and production environments * **OpenTelemetry** traces, metrics, and logs with the Grafana LGTM stack * **Auto-generated SDKs** (Python and TypeScript) from OpenAPI specs ## Platform at a Glance | Layer | Components | Key Technologies | | ------------------ | --------------------------------------------- | ----------------------------------------------------- | | **Solutions** | Data Insights, Data Governance, Semiconductor | FastAPI, A2A Protocol, LiteLLM, Prefect | | **Platform** | Governance, Assets, Utils, Runtime UI | Keycloak, OpenFGA, Secrets API, Module Federation 2.0 | | **Infrastructure** | Infrastructure Provisioning, GitOps, Helm | Terraform, ArgoCD, Kubernetes, Helm | ## Design Principles Deploy on any infrastructure you choose: your cloud, your data center, or both. The core platform components are open-source and self-hostable, and cloud-managed services are optional rather than required. For example, Redis Streams handles event streaming instead of cloud-specific message brokers, and `obstore` provides object storage across GCS, Azure Blob, S3-compatible stores, and local filesystems instead of provider-specific SDKs. The platform already includes Keycloak, OpenFGA, PostgreSQL, and Redis, plus optional secrets backends. Before adding a new dependency, the team first evaluates whether an existing one can serve the need. Every addition is justified against the cost of operational complexity. All design and implementation decisions follow these principles rigorously. Simple, focused solutions are preferred over over-engineered abstractions. ## Next Steps Understand the 3-layer architecture and how solutions depend on the platform. Learn about organizations, projects, agents, data connections, and multi-tenancy. Build, package, and deploy a new solution on CRAFT — start with the 30-minute quickstart. Explore the services that power the platform layer. Deploy CRAFT on any Kubernetes cluster, cloud-managed or self-hosted. # A2A Protocol Primer Source: https://docs.emergence.ai/guides/agent-author/a2a-protocol-primer Agent Cards, JSON-RPC methods, SSE streaming, and the task lifecycle — everything you need to understand agent communication on CRAFT. # A2A Protocol Primer Google's A2A protocol (now under the Linux Foundation) is the communication standard that connects all agents on CRAFT. Understanding A2A is prerequisite knowledge for every agent author, regardless of which framework you choose — it defines the contract your agent must honour at its network boundary. ## What A2A Is A2A is an open, vendor-neutral protocol for agent-to-agent communication. Key properties: * **Transport**: JSON-RPC 2.0 over HTTP, with Server-Sent Events (SSE) for streaming responses * **Discovery**: Agent Cards served at a well-known URL describe capabilities * **Framework-agnostic**: works with Google ADK, Pydantic AI, LangGraph, or any custom implementation * **Task-centric**: every interaction creates a Task with a defined lifecycle A2A defines the *protocol*, not the implementation. CRAFT agents are free to use any framework internally; they must speak A2A at their boundary. CRAFT's reference agents currently serve `protocolVersion: "0.3.0"` (verified against the live text2sql agent's `/.well-known/agent-card.json`). The [A2A specification](https://a2a-protocol.org/latest/specification/) reached **v1.0** in early 2026 — the platform's `a2a-sdk` Python library (`pip install a2a-sdk`) will track upstream as the SDK catches up. The `a2a-sdk` provides the canonical types and server utilities (`A2AStarletteApplication`, `DefaultRequestHandler`, `AgentCard`, etc.). ## Agent Cards — Identity and Discovery An Agent Card is a JSON document that describes what an agent does, what it can accept, and how to reach it. Every CRAFT agent publishes its card at: ``` GET /.well-known/agent-card.json ``` This endpoint is **unauthenticated** — any orchestrator can fetch it to understand the agent's capabilities before invoking it. ### Agent Card Structure ```json theme={null} { "name": "Insights Agent", "description": "Main analytics agent — plans queries, coordinates sub-agents, analyzes data.", "url": "https://", "version": "1.4.0", "protocolVersion": "0.3.0", "provider": { "organization": "EmergenceAI", "url": "https://emergence.ai" }, "capabilities": { "streaming": true, "pushNotifications": false, "stateTransitionHistory": false }, "skills": [ { "id": "data_query", "name": "Data Query", "description": "Query databases using natural language", "tags": ["sql", "query", "database"], "examples": [ "Show me Q4 revenue by region", "What are the top 10 customers by sales?" ] }, { "id": "data_analysis", "name": "Data Analysis", "description": "Analyze data, generate charts, and produce insights", "tags": ["analysis", "insights", "charts"] } ], "defaultInputModes": ["text", "data", "file"], "defaultOutputModes": ["text", "data", "file"], "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer" } }, "security": [{ "bearer": [] }] } ``` ### Capabilities | Capability | Effect when `true` | | ------------------------ | -------------------------------------------------------------------------- | | `streaming` | Agent supports `message/stream` (SSE); callers may use real-time streaming | | `pushNotifications` | Agent supports webhook push for task completion | | `stateTransitionHistory` | Agent returns full task state transition history in responses | Clients should read capabilities from the Agent Card before calling methods that depend on them. Calling `message/stream` against an agent with `streaming: false` returns `UnsupportedOperationError`. ### Skills Skills are discrete capability declarations within an Agent Card. They help orchestrators understand what tasks the agent can handle and inform LLM routing decisions. ```python theme={null} # Pydantic AI / a2a SDK — define skills in code from a2a.types import AgentSkill skill = AgentSkill( id="data_analysis", name="Data Analysis", description="Performs data analysis tasks related to semiconductors.", tags=["semiconductor", "data analysis"], examples=["analyze wafer data", "predict defect rates"], ) ``` **Claude Agent SDK and LangGraph** also require explicit skill authoring — add `AgentSkill` objects to your `AgentCard` regardless of framework: ```python theme={null} # Claude Agent SDK / LangGraph — same AgentSkill definition from a2a.types import AgentSkill skill = AgentSkill( id="enterprise-analysis", name="Enterprise Data Analysis", description="Answers business questions by querying governed data sources and generating SQL.", tags=["analytics", "sql", "enterprise"], examples=["What is our MRR by region?", "Show top 10 customers by revenue last quarter"], ) ``` **Google ADK and `to_a2a()`**: ADK's `to_a2a()` constructs a minimal Agent Card from the agent's `name`, `description`, and `instruction` — but does not populate `AgentSkill` definitions. For production CRAFT registrations, author explicit `AgentSkill` objects even when using ADK, to give orchestrators rich capability descriptions for routing decisions. **Claude Agent SDK — Wrapping as an A2A Agent:** The Claude Anthropic SDK does not natively produce A2A `AgentSkill` definitions. Wrap a Claude call inside a standard A2A server: ```python theme={null} from a2a.types import AgentSkill import anthropic client = anthropic.AsyncAnthropic() skill = AgentSkill( id="data-analysis", name="Data Analysis", description="Analyzes enterprise data using Claude and CRAFT's data catalog.", tags=["analytics", "sql", "enterprise"], examples=["What is our MRR by region?", "Identify anomalies in yesterday's orders"], ) # The A2A server calls the Claude API when tasks arrive — see streaming-idioms.mdx # for the full TaskUpdater pattern async def handle_task(message: str) -> str: response = await client.messages.create( model="claude-opus-4-8", max_tokens=1024, messages=[{"role": "user", "content": message}], ) return response.content[0].text ``` **LangGraph — Wrapping as an A2A Agent:** ```python theme={null} from a2a.types import AgentSkill from langgraph.prebuilt import create_react_agent skill = AgentSkill( id="graph-workflow", name="Graph Workflow", description="Executes multi-step analysis workflows using LangGraph state machines.", tags=["workflow", "stateful", "multi-step"], ) # Build the LangGraph agent; expose via A2AStarletteApplication (see streaming-idioms.mdx # for the TaskUpdater + astream integration pattern) graph = create_react_agent("anthropic:claude-opus-4-8", tools=[...]) ``` ## JSON-RPC Methods The A2A protocol exposes these JSON-RPC methods on your agent's root endpoint (`POST /`): | Method | Use Case | Response | | ------------------- | -------------------------------------------------- | ----------------------------------- | | `message/send` | Fire-and-forget, await a single complete response | JSON-RPC response with Task | | `message/stream` | Real-time SSE stream of status and artifact events | SSE event stream | | `tasks/get` | Poll for current task state | Task object | | `tasks/cancel` | Cancel an in-progress task | Acknowledgment | | `tasks/resubscribe` | Reconnect to a dropped SSE stream | SSE event stream from current state | Most CRAFT agents use `message/stream` exclusively — it is the preferred method for all production interactions because it delivers progressive status updates to the user. ## Message Structure Every request to a CRAFT agent is a **Message** containing one or more **Parts**: ```json theme={null} { "jsonrpc": "2.0", "method": "message/stream", "params": { "message": { "role": "user", "messageId": "", "contextId": "", "parts": [ { "kind": "text", "text": "Show me Q4 revenue by region" }, { "kind": "data", "data": { "type": "datasource", "resource_uri": "data:acme-corp:ml-project:analytics-db", "datasource_type": "database", "datasource_name": "Analytics Database", "selected_schemas": [ { "schema_name": "public", "schema_fqn": "analytics-db.analytics_db.public" } ] } } ] }, "contextId": "" }, "id": "req-1" } ``` The A2A spec uses camelCase for protocol fields (`messageId`, `contextId`, `taskId`). The platform's `a2a-sdk` server also accepts snake\_case input (`message_id`, `context_id`) and normalizes, but responses are always camelCase — match the spec to keep request/response symmetric. ### Part Types | Part Type | `kind` value | Use Case | | ---------- | ------------ | ---------------------------------------------------------------------------- | | `TextPart` | `"text"` | Natural language input or agent response | | `DataPart` | `"data"` | Structured JSON — datasource context, artifact references, selection context | | `FilePart` | `"file"` | Binary content referenced by URI (`asset://artifacts/chart.png`) | The `context_id` groups messages into a conversation session. CRAFT agents use this to retrieve conversation history from the task store and maintain coherent multi-turn dialogue. ## Task Lifecycle Every `message/send` or `message/stream` call creates or resumes a **Task**. Tasks follow a strict state machine: ``` submitted ──► working ──► completed ──► failed ──► input-required ──► canceled ──► rejected ``` Terminal states (`completed`, `failed`, `canceled`, `rejected`) are final — a task in a terminal state cannot be restarted. To continue a conversation, the client sends a new message with the same `context_id`. ## SSE Streaming — Event Sequence When a client calls `message/stream`, the server responds with `Content-Type: text/event-stream` and pushes events as the agent works. Each `data:` line is a complete JSON-RPC 2.0 response object. ### Event Types | Result Type | When Used | | ------------------------- | ---------------------------------------------------- | | `Task` | First event — the created task (state = `submitted`) | | `TaskStatusUpdateEvent` | Lifecycle state changes and intermediate messages | | `TaskArtifactUpdateEvent` | Completed artifacts (results, charts, files) | ### Protocol Rules 1. The first event MUST be a `Task` or a `Message` 2. Intermediate events are `TaskStatusUpdateEvent` (state=`working`) and `TaskArtifactUpdateEvent` 3. The final event MUST be `TaskStatusUpdateEvent` with `final: true` and a terminal state 4. The SSE connection closes after the final event ### Wire Example — Text2SQL Stream The following is a real-world event sequence from the CRAFT text2sql agent: ``` data: {"kind":"status-update","task_id":"t1","final":false, "status":{"state":"working","message":{"role":"agent", "parts":[{"kind":"text","text":"Analyzing database schema"}]}}} data: {"kind":"status-update","task_id":"t1","final":false, "status":{"state":"working","message":{"role":"agent", "parts":[{"kind":"text","text":"Running query"}]}}} data: {"kind":"artifact-update","task_id":"t1", "artifact":{"artifact_id":"abc","name":"query_summary", "parts":[{"kind":"text","text":"Top 10 customers by revenue"}]}} data: {"kind":"artifact-update","task_id":"t1","last_chunk":true, "artifact":{"artifact_id":"def","name":"query_results", "parts":[{"kind":"data","data":{"type":"artifact", "uri":"data:acme:proj:results-parquet", "resource_type":"parquet", "metadata":{"row_count":10,"columns":["customer_name","total_revenue"]}}}]}} data: {"kind":"status-update","task_id":"t1","final":true, "status":{"state":"completed"}} ``` ### Chunked Artifact Streaming Large artifacts can be delivered in chunks using the `append` and `lastChunk` flags: ```json theme={null} {"artifactUpdate": {"artifact": {"artifactId": "a1", "parts": [...chunk1...], "append": false, "lastChunk": false}}} {"artifactUpdate": {"artifact": {"artifactId": "a1", "parts": [...chunk2...], "append": true, "lastChunk": false}}} {"artifactUpdate": {"artifact": {"artifactId": "a1", "parts": [...chunk3...], "append": true, "lastChunk": true}}} ``` ## Agent-to-Agent Communication Flow The following sequence diagram shows how an orchestrator delegates a task to a sub-agent using the A2A protocol. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% sequenceDiagram participant UI as UI / Client participant Orch as Orchestrator Agent participant Reg as Assets Registry participant Sub as Sub-Agent (e.g. text2sql) UI->>Orch: POST / message/stream note over Orch: Task submitted Orch->>Reg: GET /assets/agents (resolve sub-agent URL) Reg-->>Orch: agent_card.json (url, capabilities, skills) Orch->>Sub: POST / message/stream (A2A JSON-RPC) Sub-->>Orch: SSE: Task{state=submitted} Sub-->>Orch: SSE: StatusUpdate{state=working, "Analyzing schema"} Sub-->>Orch: SSE: ArtifactUpdate{name=query_results, last_chunk=true} Sub-->>Orch: SSE: StatusUpdate{state=completed, final=true} Orch-->>UI: SSE: StatusUpdate{state=working, "Running analysis"} Orch-->>UI: SSE: ArtifactUpdate{chart + summary} Orch-->>UI: SSE: StatusUpdate{state=completed, final=true} ``` ## Authentication Agent Cards declare supported authentication schemes in `securitySchemes`. CRAFT agents use JWT bearer tokens — the orchestrator injects them when calling sub-agents. ```json theme={null} { "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } }, "security": [{ "bearer": [] }] } ``` Agents validate the token, extract the `user_id` and `org_id`, and use them to scope all database queries and audit logs. A small `UserContextBuilder` that reads the `Authorization` header and exposes a typed context object to your dependencies is the canonical pattern for Pydantic AI + `a2a` server integrations. ## Reconnection If an SSE connection drops mid-stream, the client can reconnect using `tasks/resubscribe`: ```json theme={null} { "jsonrpc": "2.0", "method": "tasks/resubscribe", "params": { "taskId": "task-123" } } ``` The server resumes the event stream from the task's current state. Your agent implementation does not need to handle this explicitly — `A2AStarletteApplication` and `DefaultRequestHandler` manage reconnection automatically. ## Next Steps Build your first agent using Google ADK, Pydantic AI, or another supported framework. Orchestration, delegation, and fan-out across multiple A2A agents. # Debugging Agents Source: https://docs.emergence.ai/guides/agent-author/debugging-agents Trace inspection, prompt iteration, tool-call analysis, and symptom-keyed troubleshooting for CRAFT agents. # Debugging Agents Agent failures are rarely obvious from error messages alone. This page covers how to inspect traces, analyse tool calls, iterate on prompts, and diagnose the most common failure modes. ## Toolbox Before debugging, confirm you have access to these tools: | Tool | Purpose | | ----------------------------------------------- | ---------------------------------------------------------------------------- | | **Langfuse** | Trace inspection, span hierarchy, token usage, LLM input/output | | **ADK Web** (`adk web`) | Interactive ADK agent debug UI — replay conversations, inspect tool calls | | **curl / jq** | Direct A2A JSON-RPC invocation for isolated testing | | **Agent Card** (`/.well-known/agent-card.json`) | Verify capabilities and skills are declared correctly | | **Application logs** | Structured JSON logs with `task_id`, `context_id`, `user_id` for correlation | ## Reading a Trace in Langfuse Every agent request produces a trace in Langfuse. The span hierarchy for a Pydantic AI agent looks like: ``` agent.execute # outer span (task_id, context_id, user_id) agent.llm_stream [attempt=0] # LLM call span mcp.tool.get_schema # MCP tool span mcp.tool.execute_sql # MCP tool span mcp.tool.upload_artifact # MCP tool span ``` Key attributes to check: * `agent.response_length` — if 0, the agent produced no output (likely an error) * `mcp.tool.status` — `success` or error classification * `mcp.tool.result_size_bytes` — large values (>50KB) indicate context bloat risk * `gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens` — token budget ## Symptom Index **Most likely causes:** 1. **Task validation failure** — the request is missing `task_id`, `context_id`, or authenticated user context. Check application logs for `ValueError: Task ID and Context ID must be provided`. 2. **Authentication error** — the JWT token is missing or invalid. If your executor validates the `Authorization` header before processing the task, a missing or invalid token raises before emitting any events. 3. **MCP connection failure** — the `StreamableHttpTransport` cannot connect to the MCP server. Check that `MCP_SERVER_URL` is set and the MCP pod is healthy. **Diagnostics:** ```bash theme={null} # Substitute :8003 with whatever port your A2A agent binds locally — # each agent listens on its own port (unlike em-runtime services, which # all bind container port 8000 and are fronted by the gateway). For the # reference em-talk2data agents these are :8001 text2sql, :8002 insights, # :8003 coding, :8080 talk2data-service. # # Check agent card — if this fails, the agent isn't running curl http://localhost:8003/.well-known/agent-card.json # Send a minimal test request curl -X POST http://localhost:8003/ \ -H "Content-Type: application/json" \ -H "x-user-id: test-user" \ -d '{ "jsonrpc":"2.0","method":"message/send", "params":{"message":{"role":"user","message_id":"m1","context_id":"c1", "parts":[{"kind":"text","text":"hello"}]}}, "id":"1" }' # Check agent logs kubectl logs -l app=my-agent --tail=100 | grep ERROR ``` **Most likely causes:** 1. **MCP server returning errors** — the tool call reaches the MCP server but returns a structured error. Check `mcp.tool.status` in the trace span. 2. **Iteration limit hit** — the agent has exceeded `max_code_failures`. Look for log line: `Iteration limit reached for task {task_id}`. 3. **Forbidden operation** — the tool call uses a restricted pattern (e.g., filesystem access, blocked import). Look for `LINT_ERROR` in the tool result. 4. **Tool schema mismatch** — the LLM is passing incorrect argument types. Check the `mcp.tool.param_fingerprint` across calls — if it's consistent and always failing, the tool schema is wrong. **Diagnostics:** ```python theme={null} # Test the tool directly, bypassing the agent from fastmcp import Client from fastmcp.client.transports import StreamableHttpTransport async def test_tool(): client = Client(transport=StreamableHttpTransport("https://craft.emergence.ai/mcp", headers={"Authorization": "Bearer ", "X-Project-ID": ""})) async with client: result = await client.call_tool( name="get_schema", arguments={"table_name": "orders", "schema_fqn": "db.schema.public"}, ) print(result) ``` **Symptoms:** Agent responses become shorter, less accurate, or the LLM refuses to call tools. Token usage approaches the model's context limit. **Most likely causes:** 1. **Tool results too large** — a tool is returning large payloads (DataFrames, Plotly figures) directly to the LLM. Your toolset should strip large fields and store them in the Assets API, passing only the resource URI. 2. **Conversation history too long** — the task store is loading the full conversation history. Check `agent.context_metrics.history_messages` in the trace. 3. **System prompt too large** — the instruction builder is including too much context. Check `agent.context_metrics.instruction_length`. **Diagnostics:** ```bash theme={null} # Check token usage in Langfuse — look for input_tokens near model limit # Gemini Flash: 1M tokens; Gemini Pro: 2M tokens; Claude Sonnet: 200K tokens # Check tool result sizes in the trace # mcp.tool.result_size_bytes > 50000 is a warning sign ``` **Fix:** Implement side payload interception in your toolset. Strip large data blobs (DataFrames, images, Plotly figures) before they reach the LLM context window; store them in the Assets API and pass only the resource URI. **Symptoms:** A single agent request consumes 10x the expected tokens. The LLM is looping on tool calls or generating excessively long responses. **Most likely causes:** 1. **Missing iteration limit** — no `max_code_failures` guard on code execution tool. The LLM keeps trying different code variations. 2. **Tool always returning errors** — the LLM keeps retrying a broken tool. Check `mcp.tool.status` across the trace — all `error` with the same tool name is a signal. 3. **Infinite delegation loop** — two agents are delegating to each other. Check the orchestrator's `sub_agents` list for circular references. 4. **Large system prompt being rebuilt per turn** — the instruction builder is fetching context on every LLM round-trip. Check `agent.context_metrics.instruction_build_duration_s`. **Diagnostics:** ```bash theme={null} # Count tool calls per task from Langfuse # agent.tool_calls_per_request > 20 is suspicious for most agents ``` **Fix:** ```python theme={null} # Add hard iteration limits if ctx.deps._code_failure_count >= settings.max_code_failures: return { "success": False, "error": "Execution limit reached. Ask the user for clarification.", } # Set max_tokens to bound output cost model_settings = ModelSettings( temperature=0, max_tokens=4096, # bound output ) ``` **Symptoms:** `state=failed` with message "No datasource DataPart found" or "resource\_uri resolution failed". **Most likely causes:** 1. **Missing DataPart in the message** — the caller did not include a `DataPart` with `type: "datasource"`. The A2A message must include both a `TextPart` and a `DataPart`. 2. **Wrong resource\_uri format** — the `resource_uri` must be in the full four-segment format: `data:{org_id}:{project_id}:{name}`. The simplified format (`data:my-db`) is not accepted. 3. **Missing selected\_schemas** — `selected_schemas` is empty or absent. Text2SQL requires exactly one schema entry. **Diagnostics:** ```bash theme={null} # Inspect the incoming A2A message for DataParts kubectl logs -l app= | grep "datasource" # Test with a complete message curl -X POST https:/// \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0","method":"message/send", "params":{"message":{"role":"user","message_id":"m1","context_id":"c1","parts":[ {"kind":"text","text":"count rows in orders"}, {"kind":"data","data":{ "type":"datasource", "resource_uri":"data:acme:proj:analytics-db", "datasource_type":"database", "datasource_name":"Analytics DB", "selected_schemas":[{"schema_name":"public","schema_fqn":"db.db.public"}] }} ]}},"id":"1" }' ``` ## Prompt Iteration The fastest way to improve agent quality is iterating on the system prompt. Use ADK Web or direct A2A calls to test prompt changes without redeploying. ### ADK Web — Interactive Replay (Google ADK) ```bash theme={null} # Start your agent in development mode uvicorn my_agent.agent:a2a_app --host 0.0.0.0 --port 8003 --reload # Open the ADK Web UI in a second terminal adk web packages/my_agent # Navigate to http://localhost:8000 and replay test conversations ``` ADK Web shows each tool call, its arguments, and the LLM's reasoning before and after. Use it to observe exactly how the prompt influences routing and tool selection. ### Claude Agent SDK — Prompt Replay For Claude-based agents, replay prompt variations using the Anthropic SDK directly without running the full A2A server: ```python theme={null} import anthropic client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, system="[paste your agent's system prompt here]", messages=[{"role": "user", "content": "[failing input]"}], tools=[...], # paste your tool definitions ) print(response.content) ``` Trace inspection: look for `anthropic.messages.create` spans in Langfuse. Tool use blocks appear as child spans with `input` and `output` fields. ### LangGraph — Debug with `debug=True` LangGraph's `astream` supports verbose debug output and LangSmith/Langfuse tracing: ```python theme={null} # Enable verbose node-level output during development async for step in graph.astream( {"messages": [{"role": "user", "content": "..."}]}, stream_mode="updates", debug=True, ): for node_name, output in step.items(): print(f"[{node_name}]", output) ``` For production tracing, pass the `langfuse_handler` callback (see [Eval Harness](/guides/agent-author/eval-harness)) — spans appear as `langgraph:node:` entries in Langfuse. ### Minimal Repro with Direct curl For non-ADK agents, replay failing conversations directly: ```bash theme={null} # Capture a failing trace from Langfuse (input field) # Replay it directly against the agent curl -X POST http://localhost:8003/ \ -H "Content-Type: application/json" \ -H "x-user-id: debug-user" \ -d @failing_trace_input.json | python3 -m json.tool ``` ### Prompt Change Checklist Before changing the system prompt: 1. Identify the specific behaviour to change (use a Langfuse trace as evidence) 2. Write a test case that captures the failure 3. Make the minimum prompt change needed to fix the test case 4. Run the full regression suite to check for new regressions 5. Re-check token usage — prompt changes can inflate or deflate input token cost ## Structured Logging for Correlation All CRAFT agents log structured JSON with `task_id` and `context_id`. Use these to correlate logs with Langfuse traces: ```python theme={null} logger.info( "Tool call completed", extra={ "tool_name": name, "task_id": task_id, "context_id": context_id, "duration_s": duration, "status": "success", }, ) ``` ```bash theme={null} # Find the Langfuse trace for the same task # Search by task_id in Langfuse UI filter: trace.metadata.task_id = "task-uuid-here" ``` ## Next Steps Build regression suites to catch issues before they reach production. Configure Langfuse tracing for your agent. # Eval Harness Source: https://docs.emergence.ai/guides/agent-author/eval-harness Langfuse evaluators, regression suites, and golden traces for CRAFT agents. # Eval Harness Agent quality degrades silently. A prompt change, a model upgrade, or a new tool can alter behaviour across hundreds of evaluation dimensions without surfacing as an exception. A structured eval harness catches regressions before they reach users. CRAFT uses Langfuse for observability and evaluation. Before setting up your eval harness, complete the [Langfuse Setup Guide](/guides/langfuse-setup) to connect your agent to your Langfuse project. ## What to Evaluate For each agent, define evaluations in three categories: | Category | Description | Example | | -------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- | | **Functional correctness** | Does the agent produce the right output for a given input? | SQL matches expected query, chart type matches request | | **Tool-call accuracy** | Does the agent call the right tools with correct arguments? | Calls `get_schema` before `execute_sql`; passes correct `schema_fqn` | | **Conversation behaviour** | Does the agent handle edge cases gracefully? | Asks for clarification when datasource is missing; does not loop indefinitely | ## Setting Up Langfuse Tracing All agent frameworks on CRAFT support Langfuse tracing via the OTEL exporter. ### Pydantic AI Configure Langfuse via the standard OTEL exporter. Add this at application startup: ```python theme={null} from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint=f"https://{LANGFUSE_HOST}/api/public/otel/v1/traces", headers={"Authorization": f"Basic {langfuse_basic_auth}"}, ) ) ) trace.set_tracer_provider(provider) ``` Pydantic AI emits OTEL spans automatically when a `TracerProvider` is configured. Every agent run, tool call, and MCP request appears as a structured span. ### Google ADK Configure Langfuse via the standard OTEL exporter: ```python theme={null} from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint=f"https://{LANGFUSE_HOST}/api/public/otel/v1/traces", headers={"Authorization": f"Basic {langfuse_basic_auth}"}, ) ) ) trace.set_tracer_provider(provider) ``` ADK emits OTEL spans for each agent turn, tool invocation, and LLM call. Look for `google.adk.agent` root spans in Langfuse. ### Claude Agent SDK The Anthropic SDK emits OTEL spans automatically when a `TracerProvider` is configured — the same setup as Pydantic AI and ADK works without additional configuration: ```python theme={null} from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from anthropic import Anthropic provider = TracerProvider() provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint=f"https://{LANGFUSE_HOST}/api/public/otel/v1/traces", headers={"Authorization": f"Basic {langfuse_basic_auth}"}, ) ) ) trace.set_tracer_provider(provider) # Anthropic SDK auto-emits spans (anthropic.messages.create root) when TracerProvider is set client = Anthropic() ``` Look for `anthropic.messages.create` root spans in Langfuse, with `tool_use` child spans for each tool invocation. ### LangGraph LangGraph integrates with Langfuse via the LangChain callback handler (requires `langfuse>=3.0`): ```python theme={null} # Requires: pip install langfuse>=3.0 from langfuse.langchain import CallbackHandler # v3 import path (NOT langfuse.callback) from langgraph.prebuilt import create_react_agent langfuse_handler = CallbackHandler() # reads LANGFUSE_PUBLIC_KEY/SECRET_KEY from env graph = create_react_agent("anthropic:claude-opus-4-8", tools=[...]) # Pass handler per-invocation result = await graph.ainvoke( {"messages": [{"role": "user", "content": "..."}]}, config={"callbacks": [langfuse_handler]}, ) ``` Spans appear in Langfuse as `langgraph:node:` entries. Each node execution, LLM call, and tool invocation is captured separately. ## Golden Traces A golden trace is a recorded agent execution that represents the correct behaviour for a given input. Store golden traces in your test fixtures and replay them to catch regressions. ### Recording a Golden Trace ```python theme={null} import json from langfuse import Langfuse langfuse = Langfuse() # Fetch a specific trace by ID from Langfuse trace = langfuse.fetch_trace("trace-id-from-langfuse-ui") # Serialize to a fixture file golden = { "input": trace.input, "expected_output": trace.output, "tool_calls": [ {"name": span.name, "input": span.input, "output": span.output} for span in trace.observations if span.type == "SPAN" and span.name.startswith("mcp.tool.") ], } with open("tests/fixtures/golden_revenue_query.json", "w") as f: json.dump(golden, f, indent=2) ``` ### Replaying Golden Traces ```python theme={null} import pytest import json from my_agent.executor import MyAgentExecutor from tests.harness import MockEventQueue, MockRequestContext @pytest.mark.parametrize("fixture_path", [ "tests/fixtures/golden_revenue_query.json", "tests/fixtures/golden_top_customers.json", "tests/fixtures/golden_missing_datasource.json", ]) async def test_golden_trace(fixture_path): with open(fixture_path) as f: golden = json.load(f) executor = MyAgentExecutor(...) event_queue = MockEventQueue() context = MockRequestContext(user_input=golden["input"]) await executor.execute(context, event_queue) # Check final output final_event = event_queue.get_final_event() assert golden["expected_output"] in final_event.text # Check tool calls actual_tool_calls = event_queue.get_tool_calls() for expected in golden["tool_calls"]: assert any( tc["name"] == expected["name"] for tc in actual_tool_calls ), f"Expected tool call '{expected['name']}' not found" ``` ## Langfuse Evaluators Langfuse evaluators run scoring functions against traces. CRAFT agents use two types: ### LLM-as-Judge Evaluators Use a lightweight LLM to score agent outputs on dimensions like correctness, helpfulness, and groundedness: ```python theme={null} from langfuse import Langfuse from langfuse.model import ModelUsage langfuse = Langfuse() def create_sql_correctness_evaluator(): """Create a Langfuse evaluator that scores SQL correctness.""" return langfuse.create_score_config( name="sql_correctness", data_type="NUMERIC", min_value=0, max_value=1, ) def score_sql_output(trace_id: str, generated_sql: str, expected_sql: str): """Score a trace using an LLM judge.""" import anthropic client = anthropic.Anthropic() response = client.messages.create( model="claude-haiku-4-5", max_tokens=256, messages=[{ "role": "user", "content": f"""Score the SQL query correctness from 0 to 1. Expected SQL: {expected_sql} Generated SQL: {generated_sql} Return only a JSON object: {{"score": 0.95, "reason": "..."}}""" }], ) result = json.loads(response.content[0].text) langfuse.score( trace_id=trace_id, name="sql_correctness", value=result["score"], comment=result["reason"], ) ``` ### Deterministic Evaluators For structured outputs where correctness can be computed without an LLM: ```python theme={null} def score_tool_call_order(trace_id: str, tool_calls: list[dict]): """Score whether the agent called tools in the correct order.""" names = [tc["name"] for tc in tool_calls] # get_schema must precede execute_sql if "execute_sql" in names and "get_schema" in names: schema_idx = names.index("get_schema") sql_idx = names.index("execute_sql") score = 1.0 if schema_idx < sql_idx else 0.0 elif "execute_sql" in names: score = 0.0 # called execute_sql without schema check else: score = 1.0 # no SQL — correct for non-SQL queries langfuse.score( trace_id=trace_id, name="tool_call_order", value=score, ) ``` ## Regression Suite Structure Organise your test fixtures by scenario type. For the text2sql agent, a well-structured regression suite includes: ```text theme={null} tests/ ├── fixtures/ │ ├── happy_path/ │ │ ├── simple_count.json │ │ ├── revenue_by_quarter.json │ │ └── top_customers.json │ ├── edge_cases/ │ │ ├── missing_datasource.json # Agent should return state=failed │ │ ├── unsupported_dialect.json │ │ └── empty_result_set.json │ └── cancellation/ │ └── cancel_mid_execution.json ├── test_golden_traces.py ├── test_tool_call_order.py └── harness.py # MockEventQueue, MockRequestContext ``` ## Running Evals in CI Add eval runs to your CI pipeline to catch regressions on pull requests: ```yaml theme={null} # .github/workflows/eval.yml name: Agent Eval on: pull_request: paths: - "packages/my_agent/**" jobs: eval: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run regression suite env: LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} run: | uv run pytest tests/test_golden_traces.py -v \ --tb=short \ -x # fail fast on first regression ``` ## Cost-Aware Evaluation Track token usage per eval run to catch cost regressions alongside quality regressions: ```python theme={null} # After each eval run, log usage to Langfuse langfuse.score( trace_id=trace_id, name="input_tokens", value=usage.input_tokens, data_type="NUMERIC", ) langfuse.score( trace_id=trace_id, name="output_tokens", value=usage.output_tokens, data_type="NUMERIC", ) langfuse.score( trace_id=trace_id, name="tool_call_count", value=toolset.tool_call_count, data_type="NUMERIC", ) ``` Set budget alerts in Langfuse if token usage per task exceeds a threshold — this catches prompt regressions that inflate input context or cause tool call loops. ## Next Steps Inspect traces and diagnose failures when evals surface regressions. # Multi-Agent Patterns Source: https://docs.emergence.ai/guides/agent-author/multi-agent-patterns Delegation, supervision, and parallel fan-out across CRAFT agents using the A2A protocol. # Multi-Agent Patterns CRAFT's multi-agent coordination happens at the protocol layer through A2A. Agents discover each other via the registry, delegate work via JSON-RPC, and stream results via SSE — without sharing process memory or tight framework coupling. This page covers the three main coordination patterns used in production CRAFT deployments. ## The CRAFT Multi-Agent Architecture CRAFT's analytics solution is a good reference for multi-agent patterns. A typical layout chains three specialized agents: 1. **Orchestrator** — receives user requests, plans the workflow, delegates to sub-agents 2. **Text2SQL** — converts natural language to SQL, executes against the target database, returns query results as Parquet artifacts 3. **Insights** — analyses query results, generates charts, produces natural-language insights and visualizations Each agent is an independently deployed microservice. They communicate exclusively through the A2A protocol. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR UI["User / UI"] -->|"REST + SSE"| Facade["Analytics Service\n(REST facade)"] Facade -->|"A2A message/stream"| Orch["Orchestrator Agent\n(Google ADK)"] Orch -->|"A2A delegate"| T2S["Text2SQL Agent\n(ADK)"] Orch -->|"A2A delegate"| Ins["Insights Agent\n(ADK)"] Orch -->|"A2A delegate"| Future["Future Agents\n(any framework)"] T2S -->|"SSE: sql_plan, query_results"| Orch Ins -->|"SSE: chart, summary"| Orch T2S --- DB[("Database")] Ins --- Artifacts[("Artifact Store")] classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef process fill:#E69F00,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef external fill:#CC79A7,stroke:#555555,color:#000 classDef data fill:#009E73,stroke:#555555,color:#000 class UI entry class Facade process class Orch,T2S,Ins process class Future external class DB,Artifacts,Artifacts data ``` ## Pattern 1: Sequential Delegation The orchestrator delegates tasks to sub-agents one at a time, using the output of one as the input to the next. This is the most common pattern on CRAFT. ### Google ADK — `RemoteA2aAgent` Sub-Agents ADK makes sequential delegation transparent: `RemoteA2aAgent` instances look like local sub-agents. The orchestrator LLM routes to them via the `transfer_to_agent` tool. Resolve sub-agent URLs from the CRAFT Assets API rather than hardcoding hostnames. ```python theme={null} from google.adk.agents.remote_a2a_agent import RemoteA2aAgent from google.adk.agents.llm_agent import Agent # Resolve agent card URLs from the Assets API at startup text2sql = RemoteA2aAgent( name="text2sql_agent", description="Converts natural language to SQL queries and executes them.", agent_card="https:///.well-known/agent-card.json", ) insights = RemoteA2aAgent( name="insights_agent", description="Analyses query results, generates charts, and produces insights.", agent_card="https:///.well-known/agent-card.json", ) orchestrator = Agent( model="gemini-3.5-flash", name="orchestrator", instruction=""" You are a data analysis orchestrator. When the user asks a data question: 1. Delegate to text2sql_agent to retrieve the data 2. Delegate to insights_agent to analyse and visualise it 3. Synthesize the results for the user Always pass the datasource context (resource_uri, schema) to text2sql_agent. """, sub_agents=[text2sql, insights], ) ``` ### Claude Agent SDK — Sequential Delegation ```python theme={null} import httpx import anthropic from a2a.client import A2AClient from a2a.types import SendMessageRequest, MessageSendParams, Message, Part, TextPart, Role from uuid import uuid4 claude = anthropic.Anthropic() async def claude_orchestrator(user_question: str, sub_agent_url: str, context_id: str) -> str: async with httpx.AsyncClient() as http: a2a = A2AClient(http, url=sub_agent_url) # Delegate to sub-agent via A2A request = SendMessageRequest( id=str(uuid4()), params=MessageSendParams( message=Message( role=Role.user, message_id=str(uuid4()), context_id=context_id, parts=[Part(root=TextPart(text=user_question))] ) ) ) result = await a2a.send_message(request) sub_result = "" if result.root.result and result.root.result.artifacts: for artifact in result.root.result.artifacts: for part in artifact.parts: if hasattr(part.root, "text"): sub_result += part.root.text # Claude synthesises the final response response = claude.messages.create( model="claude-opus-4-8", max_tokens=1024, messages=[{ "role": "user", "content": f"Original question: {user_question}\nSub-agent result: {sub_result}\n\nSynthesize a final answer." }], ) return response.content[0].text ``` ### LangGraph — Sequential Delegation ```python theme={null} from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator import httpx from a2a.client import A2AClient from a2a.types import SendMessageRequest, MessageSendParams, Message, Part, TextPart, Role from uuid import uuid4 class OrchestratorState(TypedDict): question: str context_id: str sub_agent_result: str final_answer: str async def delegate_to_sub_agent(state: OrchestratorState) -> OrchestratorState: async with httpx.AsyncClient() as http: a2a = A2AClient(http, url="https://") request = SendMessageRequest( id=str(uuid4()), params=MessageSendParams( message=Message( role=Role.user, message_id=str(uuid4()), context_id=state["context_id"], parts=[Part(root=TextPart(text=state["question"]))] ) ) ) result = await a2a.send_message(request) sub_text = "" if result.root.result and result.root.result.artifacts: for artifact in result.root.result.artifacts: for part in artifact.parts: if hasattr(part.root, "text"): sub_text += part.root.text return {"sub_agent_result": sub_text} builder = StateGraph(OrchestratorState) builder.add_node("delegate", delegate_to_sub_agent) builder.set_entry_point("delegate") builder.add_edge("delegate", END) graph = builder.compile() ``` ### How ADK Delegation Works When the orchestrator LLM decides to delegate: 1. LLM emits a `transfer_to_agent` tool call with the target agent name and message 2. ADK resolves the remote agent's card (lazy, cached after first fetch) 3. ADK constructs an A2A `message/stream` JSON-RPC request from the session context 4. Sends via `httpx.AsyncClient` to the sub-agent's URL 5. Processes the SSE response stream back into ADK internal events 6. Returns the final artifact to the orchestrator as if it were a local sub-agent result ### Passing Context Between Agents The Text2SQL agent requires a `DataPart` with the datasource context. The orchestrator must forward this from the original user message: ```json theme={null} { "method": "message/stream", "params": { "message": { "parts": [ { "kind": "text", "text": "Show me top 10 customers by revenue" }, { "kind": "data", "data": { "type": "datasource", "resource_uri": "data:acme-corp:ml-project:analytics-db", "datasource_type": "database", "datasource_name": "Analytics Database", "selected_schemas": [ { "schema_name": "public", "schema_fqn": "analytics-db.analytics_db.public" } ] } } ] } } } ``` The `resource_uri` must be in the full four-segment format: `data:{org_id}:{project_id}:{name}`. The simplified format (`data:my-db`) will fail — the Assets API requires all four segments for connection resolution. ## Pattern 2: Parallel Fan-Out Multiple sub-agents are invoked concurrently and their results are merged. Use this when sub-tasks are independent and latency matters. ### Manual Fan-Out with asyncio For frameworks other than ADK (or for orchestrators that need explicit concurrency control): ```python theme={null} import asyncio import httpx from a2a.client import A2AClient async def fan_out_query( context_id: str, user_message: str, agent_urls: list[str], ) -> list[dict]: """Invoke multiple agents in parallel and collect their results.""" async def invoke_agent(url: str) -> dict: async with httpx.AsyncClient() as http: a2a = A2AClient(http, url=url) # Full shape: see a2a-protocol-primer.mdx for the SendMessageRequest + Message/Part structure # Simplified for readability — replace with: # request = SendMessageRequest(id=str(uuid4()), params=MessageSendParams( # message=Message(role=Role.user, message_id=str(uuid4()), # context_id=context_id, parts=[Part(root=TextPart(text=user_message))]))) result = await a2a.send_message( message=user_message, context_id=context_id, ) return result tasks = [invoke_agent(url) for url in agent_urls] results = await asyncio.gather(*tasks, return_exceptions=True) # Filter out failures — fan-out should be resilient to partial failures return [r for r in results if not isinstance(r, Exception)] ``` ### ADK Fan-Out via Parallel Sub-Agents Google ADK supports parallel sub-agent invocation when the orchestrator instruction explicitly requests concurrent delegation: ```python theme={null} orchestrator = Agent( model="gemini-3.5-flash", name="orchestrator", instruction=""" You can delegate to multiple agents simultaneously when tasks are independent. For data analysis requests that need both SQL results and market context, invoke text2sql_agent and context_agent in parallel, then synthesize results. """, sub_agents=[text2sql, context_agent], ) ``` ADK's `RemoteA2aAgent` A2A support is currently **experimental**. Parallel invocation of multiple `RemoteA2aAgent` instances in the same turn may produce duplicate events (tracked in [ADK issue #3207](https://github.com/google/adk-python/issues/3207)). Test thoroughly before using in production. ## Pattern 3: Supervision and Retry An orchestrator supervises sub-agent results and retries failed tasks with corrected inputs. ```python theme={null} orchestrator = Agent( model="gemini-3.5-flash", name="orchestrator", instruction=""" When text2sql_agent returns a failed task: 1. Inspect the error message 2. If it's a schema error, call text2sql_agent again with corrected schema context 3. If it's a permission error, ask the user to check their database access 4. Maximum 2 retries before reporting failure to the user Never retry more than twice — report the issue instead of looping indefinitely. """, sub_agents=[text2sql], ) ``` ### Detecting Sub-Agent Failures A sub-agent that fails emits a `TaskStatusUpdateEvent` with `state=failed` and an error message. ADK surfaces this as an error event in the orchestrator's event stream. The orchestrator LLM sees the failure message in its context and can decide to retry or escalate. ## Registering Sub-Agents with the Orchestrator When adding a new agent to the platform, you must: 1. **Register it with the Assets API** — gives it a `resource_uri` and makes it discoverable 2. **Add it as a `RemoteA2aAgent` to the orchestrator** (if using Google ADK) — and redeploy 3. **Update the orchestrator instruction** — explain what the new agent does and when to use it ```python theme={null} # Add to orchestrator's sub_agents list new_agent = RemoteA2aAgent( name="my_new_agent", description="Describe what this agent does and when the orchestrator should use it.", agent_card="https:///.well-known/agent-card.json", ) orchestrator = Agent( ... sub_agents=[text2sql, insights, new_agent], # add here ) ``` The `description` field on `RemoteA2aAgent` is what the orchestrator LLM reads to decide routing. Write it as a capability statement: "Use this agent when the user needs X, Y, or Z." ## Carrying Artifacts Across Agent Boundaries Artifacts (Parquet files, charts, SQL queries) produced by sub-agents are stored in the Assets API and referenced by `resource_uri`. When the orchestrator passes results from one agent to another, it passes the `resource_uri` — not the raw data. ```python theme={null} # Text2SQL produces query_results as an ArtifactFilePart { "type": "artifact", "uri": "data:acme-corp:ml-project:query-results-uuid", "resource_type": "parquet", "metadata": {"row_count": 100, "columns": ["customer", "revenue"]} } # Orchestrator passes this URI to Insights Agent as a DataPart { "kind": "data", "data": { "type": "artifact_context", "uri": "data:acme-corp:ml-project:query-results-uuid", "resource_type": "parquet" } } ``` ## Context Propagation Every A2A message must include a `context_id` — the conversation session identifier. Sub-agents use this to look up conversation history from the task store. In a multi-agent architecture, the orchestrator's `context_id` flows through to each sub-agent call. This ensures that sub-agents can retrieve prior turns when needed for multi-step workflows. ```python theme={null} # ADK handles context_id propagation automatically via RemoteA2aAgent. # For manual A2A calls, always forward the parent context_id: response = await a2a_client.send_message( message=user_message, context_id=parent_context_id, # always forward ) ``` ## Next Steps SSE streaming, partial results, and cooperative cancellation. Review the A2A wire format and task lifecycle. # Agent Author Guide Source: https://docs.emergence.ai/guides/agent-author/overview How to build, register, and operate A2A-compatible agents on the CRAFT platform. # Agent Author Guide CRAFT is a multi-agent platform built on the A2A protocol. Every capability in CRAFT — from natural-language SQL generation to semiconductor yield analysis — runs as a discrete, independently deployable agent. **The short version:** CRAFT gives you managed LLMs, tool discovery, and an agent registry. Your agent gets all three by pointing its LLM client at the CRAFT gateway and making one API call to register. Start with the [tutorial](/guides/agent-author/your-first-agent) if you want to be running in under 20 minutes. This overview covers what it means to be an agent on CRAFT, when to build one, how to choose a framework, and how the platform integrates with your agent at runtime. ## What is an agent on CRAFT? A CRAFT agent is a service that: 1. Exposes an **Agent Card** at `/.well-known/agent-card.json` describing its identity, skills, and capabilities 2. Accepts work via **A2A JSON-RPC** (`message/send` or `message/stream`) 3. Streams progress and results back as **SSE events** 4. Registers with the **Assets API** so other agents and the orchestrator can discover and invoke it Agents are microservices first. They can be authored in any language, deployed anywhere, and call any tools — as long as they speak the A2A protocol at their boundary. ## When should you build an agent? * You have a bounded capability with a clear input/output contract (e.g., "turn natural language into SQL", "generate yield analysis charts") * The capability needs to be independently scalable or deployable * The capability will be invoked by other agents or orchestrators * You want the capability to appear in the platform's agent registry and be discoverable by the orchestrator * The capability is owned by a separate team with its own release cadence * The capability is a simple function call used only within a single agent — add it as a tool instead * The capability is a data transformation pipeline with no LLM reasoning — use a standard service * You need real-time collaboration between multiple LLMs sharing memory — use sub-agents within the same ADK process * The capability is a UI-layer concern — use CRAFT's REST API directly ## Framework choice CRAFT supports four agent frameworks. Choose based on your team's constraints and the nature of your agent. | Framework | Language | Best For | Pros | Cons | | -------------------- | ------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | **Google ADK** | Python | Analytics, multi-agent orchestration, data pipelines | Native A2A (`to_a2a()`), `RemoteA2aAgent` for sub-agent delegation, model-agnostic via LiteLLM, ADK Web for debugging | A2A support still experimental; Python-only | | **Claude Agent SDK** | Python, TypeScript | Reasoning-heavy tasks, document analysis, complex multi-step workflows | Extended thinking, strong tool-use, multi-turn conversation handling | No native A2A server — wrap with `a2a` library manually | | **Pydantic AI** | Python | Type-safe agents, data-structured outputs, MCP toolsets | FastMCPToolset for MCP integration, strict typing, clean dependency injection via `deps_type` | Less mature ecosystem than ADK | | **LangGraph** | Python | Stateful workflows, graph-based logic, conditional branching | Explicit state machine, fine-grained control over execution flow | More boilerplate; no native A2A — wrap with `a2a` library | **CrewAI and role-based frameworks are not supported on CRAFT.** Multi-agent coordination on CRAFT happens at the protocol layer through A2A — agents discover each other via the registry and communicate via JSON-RPC. Role-based crew frameworks duplicate this coordination mechanism and create tight coupling that breaks independent deployability. Do not use them. ### Framework decision flowchart ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD Start["New agent on CRAFT"] --> Q1{"Primary need?"} Q1 -->|"Multi-agent orchestration\nor data analytics"| Q2{"Team already uses\nGoogle ADK?"} Q1 -->|"Reasoning-heavy,\nlong documents"| Claude["Claude Agent SDK"] Q1 -->|"Strict typed outputs,\nMCP toolsets"| Pydantic["Pydantic AI"] Q1 -->|"Complex stateful\nworkflow / branching"| LangGraph["LangGraph"] Q2 -->|"Yes or no preference"| ADK["Google ADK"] Q2 -->|"No — already on\nPydantic AI"| Pydantic ADK --> A2A_native["to_a2a() + RemoteA2aAgent\n— A2A built in"] Claude --> A2A_wrap["Wrap with a2a library\n+ A2AStarletteApplication"] Pydantic --> A2A_wrap LangGraph --> A2A_wrap A2A_native --> Register["Register in Assets API"] A2A_wrap --> Register classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef decision fill:#F0E442,stroke:#555555,color:#000 classDef framework fill:#E69F00,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef terminal fill:#009E73,stroke:#555555,color:#000 class Start entry class Q1,Q2 decision class ADK,Claude,Pydantic,LangGraph framework class A2A_native,A2A_wrap infra class Register terminal ``` ## Platform integration points Every agent on CRAFT integrates with three platform services at runtime. ### Assets API — agent registry The Assets API is the source of truth for agent discovery. Before your agent can be invoked by an orchestrator, you must register it: ```bash theme={null} curl -X POST "https:///api/assets/agents" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "agent_card": { ... }, "tags": ["analytics"], "visibility": "TENANT_ONLY" }' ``` The registry stores the Agent Card, assigns a `resource_uri` (`agent:::`), and makes the agent discoverable to orchestrators and other platform consumers. ### LiteLLM gateway — model access All LLM calls from agents on CRAFT route through the shared LiteLLM gateway. This provides: * **Unified model access** — use Gemini, Claude, GPT-5.5, and others with a single OpenAI-compatible endpoint * **Cost tracking** — per-org, per-project token usage recorded automatically * **Rate limiting** — enforced per tenant to prevent runaway costs * **Model aliasing** — `LLM_PRO`, `LLM_LIGHT` constants resolve to the current best model for each tier Configure your agent to point at the gateway: ```python theme={null} # Google ADK — via the LiteLlm wrapper pointed at the gateway from google.adk.agents import Agent from google.adk.models.lite_llm import LiteLlm import os root_agent = Agent( name="my_agent", # The "openai/" prefix tells LiteLLM to use the OpenAI-compatible endpoint; # the actual model string is whatever the gateway routes (gemini-3.5-flash here). model=LiteLlm( model="openai/gemini-3.5-flash", api_base=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ), ) # Pydantic AI — point the OpenAI-compatible provider at the gateway from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider model = OpenAIChatModel( "gemini-3.5-flash", provider=OpenAIProvider( base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ), ) agent = Agent(model, system_prompt="...") ``` See [Your First Agent › Step 1](/guides/agent-author/your-first-agent) for the same pattern across all four supported frameworks (ADK, Claude SDK / OpenAI client, Pydantic AI, LangGraph). ### em-runtime-mcp — tool access CRAFT agents access data and platform capabilities through the `em-runtime-mcp` MCP server. Tools available include database schema inspection, query execution, artifact upload/download, and context graph queries. ```python theme={null} # Pydantic AI — FastMCPToolset connects to em-runtime-mcp from pydantic_ai.toolsets.fastmcp import FastMCPToolset from fastmcp import Client from fastmcp.client.transports import StreamableHttpTransport mcp_client = Client( transport=StreamableHttpTransport(settings.mcp_server_url), ) toolset = FastMCPToolset(mcp_client) ``` Create a fresh MCP client and toolset per agent request. Do not reuse a single long-lived client across requests — MCP sessions can expire or the MCP server pod can restart. ## Guide contents Agent Cards, JSON-RPC methods, SSE streaming, and the task lifecycle. Framework-by-framework tutorial: Google ADK, Claude Agent SDK, Pydantic AI, LangGraph. Function tools, MCP tools via FastMCPToolset, schema discipline. Delegation, supervision, and parallel fan-out via A2A. SSE streaming, partial results, cooperative cancellation. Langfuse evaluators, regression suites, golden traces. Trace inspection, prompt iteration, common failure modes. # Streaming Idioms Source: https://docs.emergence.ai/guides/agent-author/streaming-idioms SSE streaming, partial results, cooperative cancellation, and reconnection patterns for CRAFT agents. # Streaming Idioms All CRAFT agents stream their responses via Server-Sent Events (SSE). This page covers the practical patterns for producing well-formed SSE streams, handling partial results, and implementing cooperative cancellation. **A2A SSE vs. MCP Streamable HTTP**: This page covers A2A streaming — the JSON-RPC events that flow between agents via Server-Sent Events (SSE). This is different from the MCP Streamable HTTP transport used to connect to the CRAFT tool gateway. A2A's `message/stream` uses SSE; MCP tool connections use the MCP Streamable HTTP transport (e.g., ADK's `StreamableHTTPConnectionParams`, FastMCP's `StreamableHttpTransport`, the MCP Python SDK's `streamablehttp_client`, or LangGraph's `MultiServerMCPClient` with `transport="http"`). Both A2A SSE and MCP Streamable HTTP are current standards — they serve different protocol layers. ## The Streaming Contract The A2A protocol requires every `message/stream` response to follow a strict event ordering: 1. First event: `Task` (state = `submitted`) 2. Zero or more: `TaskStatusUpdateEvent` (state = `working`) with intermediate progress messages 3. Zero or more: `TaskArtifactUpdateEvent` with result artifacts 4. Final event: `TaskStatusUpdateEvent` with `final: true` and a terminal state (`completed`, `failed`, `canceled`) Every event is a JSON-RPC 2.0 response object sent as a `data:` line in the SSE stream. Never close the SSE connection without emitting a final event with `final: true`. Clients that receive an unexpected connection close will attempt to resubscribe via `tasks/resubscribe`, and if the task store has no record of the task, they will error. Always emit a final status event before the connection closes, even on error. ## Streaming Text Output ### Pydantic AI — `run_stream` with `stream_text()` The following pattern demonstrates streaming text output from a Pydantic AI agent. Each text chunk is forwarded as a `TaskStatusUpdateEvent` with `state=working`: ```python theme={null} response_message = updater.new_agent_message( parts=[Part(root=TextPart(text=""))] ) async with self.agent.run_stream( user_message, deps=agent_deps, toolsets=[toolset], ) as run: async for text_content in run.stream_text(): # Update the message in place — each chunk replaces the previous response_message.parts[0].root = TextPart(text=text_content) await updater.update_status( TaskState.working, message=response_message, ) # Get the final complete output and emit the terminal working event result = await run.get_output() response_message.parts[0].root = TextPart(text=result) await updater.update_status( TaskState.working, message=response_message, final=True, metadata={"timestamp": datetime.now(timezone.utc).isoformat()}, ) await updater.complete() ``` `stream_text()` returns the **accumulated** text at each yield, not a delta. Each chunk is the full response so far. This means the client always has a coherent partial response it can display immediately, without needing to concatenate chunks. ### Google ADK — SSE via `to_a2a()` When using `to_a2a()`, ADK handles all SSE event emission automatically. Your agent code just runs normally: ```python theme={null} root_agent = Agent( model="gemini-3.5-flash", name="my_agent", instruction="...", tools=[my_tool], ) # to_a2a() wraps the agent with SSE streaming support. # Intermediate LLM output appears as TaskStatusUpdateEvent (state=working). # Final output appears as TaskArtifactUpdateEvent. a2a_app = to_a2a(root_agent, port=8003) ``` ADK's `to_a2a()` uses `TaskStatusUpdateEvent` with `state=working` for streaming intermediate text chunks, then emits a `TaskArtifactUpdateEvent` for the final text. This differs from the Pydantic AI pattern (which uses only status events). Both are valid A2A; clients must handle both patterns. ### Claude Agent SDK — Streaming via A2A The Claude Agent SDK doesn't wrap natively in A2A. Build a thin A2A executor that streams from the Anthropic API and emits the correct A2A events: ```python theme={null} import anthropic from uuid import uuid4 from a2a.server.tasks import TaskUpdater from a2a.types import TaskState, Part, TextPart async def claude_a2a_handler(task_updater: TaskUpdater, user_message: str): client = anthropic.AsyncAnthropic() # AsyncAnthropic for use inside async def await task_updater.update_status( TaskState.working, message=task_updater.new_agent_message([Part(root=TextPart(text="Starting..."))]), final=False, ) full_response = "" async with client.messages.stream( model="claude-opus-4-8", max_tokens=1024, messages=[{"role": "user", "content": user_message}], ) as stream: async for text_delta in stream.text_stream: full_response += text_delta # Throttle: emit status every ~200 chars if len(full_response) % 200 < len(text_delta): await task_updater.update_status( TaskState.working, message=task_updater.new_agent_message( [Part(root=TextPart(text=full_response[:120] + "..."))] ), final=False, ) await task_updater.add_artifact( parts=[Part(root=TextPart(text=full_response))], artifact_id=str(uuid4()), name="response", last_chunk=True, ) await task_updater.update_status( TaskState.completed, message=task_updater.new_agent_message([Part(root=TextPart(text=full_response))]), final=True, ) ``` ### LangGraph — Streaming via A2A Use `stream_mode="messages"` which yields `(chunk, metadata)` tuples for token-level streaming. Use `stream_mode="updates"` for node-completion events instead. ```python theme={null} from uuid import uuid4 from a2a.server.tasks import TaskUpdater from a2a.types import TaskState, Part, TextPart async def langgraph_a2a_handler(task_updater: TaskUpdater, graph, user_message: str): await task_updater.update_status( TaskState.working, message=task_updater.new_agent_message([Part(root=TextPart(text="Running graph..."))]), final=False, ) full_output = "" # stream_mode="messages" yields (AIMessageChunk, metadata) tuples async for chunk, metadata in graph.astream( {"messages": [{"role": "user", "content": user_message}]}, stream_mode="messages", ): if hasattr(chunk, "content") and chunk.content: full_output += chunk.content if len(full_output) % 200 < len(chunk.content): await task_updater.update_status( TaskState.working, message=task_updater.new_agent_message( [Part(root=TextPart(text=full_output[:120] + "..."))] ), final=False, ) await task_updater.add_artifact( parts=[Part(root=TextPart(text=full_output))], artifact_id=str(uuid4()), name="result", last_chunk=True, ) await task_updater.update_status( TaskState.completed, message=task_updater.new_agent_message([Part(root=TextPart(text=full_output))]), final=True, ) ``` For graphs without a `messages` state key, switch to `stream_mode="updates"` and iterate `for node_name, payload in chunk.items():` to detect completed nodes. ## Streaming Intermediate Status Messages Use intermediate status messages to keep users informed during long-running tasks. Emit an intent acknowledgment before the main agent loop: ```python theme={null} # Emit a brief acknowledgment before the agent starts processing. # This confirms to the user that the request was received. ack_message = updater.new_agent_message( parts=[Part(root=TextPart(text="Analysing your request..."))] ) await updater.update_status( TaskState.working, message=ack_message, final=False ) # Then run the main agent loop... ``` For long database queries, emit named step messages: ```python theme={null} for step_name, step_fn in pipeline_steps: await updater.update_status( TaskState.working, message=updater.new_agent_message( [Part(root=TextPart(text=f"Step: {step_name}"))] ), final=False, ) await step_fn() ``` ## Artifact Streaming Artifacts (charts, data files, analysis results) are emitted as `TaskArtifactUpdateEvent`. For multi-artifact agents (like a text-to-SQL agent that produces intermediate SQL plans followed by query results), artifacts are emitted in a specific order: 1. `sql_plan` — intermediate SQL planning artifact (TextPart, JSON) 2. `query_summary` — human-readable summary of results (TextPart) 3. `sql_query` — the generated SQL (DataPart with artifact URI) 4. `query_results` — Parquet results file (DataPart with artifact URI, `last_chunk=true`) ```python theme={null} # Emit artifacts in order using the task updater await updater.add_artifact( parts=[TextPart(text=summary_text)], artifact_id=str(uuid4()), name="query_summary", ) await updater.add_artifact( parts=[DataPart(data={ "type": "artifact", "uri": parquet_resource_uri, "resource_type": "parquet", "metadata": {"row_count": row_count, "columns": column_names}, })], artifact_id=str(uuid4()), name="query_results", last_chunk=True, ) ``` ## Cooperative Cancellation CRAFT supports cooperative cancellation: a client sends `tasks/cancel`, and the agent should stop its current work and emit a `canceled` terminal event. ### Pydantic AI — asyncio.CancelledError Implement cancellation via asyncio task cancellation. The `cancel()` method updates the task state in the database before the running asyncio task receives the cancellation signal: ```python theme={null} class MyAgentExecutor(AgentExecutor): async def cancel( self, context: RequestContext, event_queue: EventQueue ) -> None: task_id = context.task_id # Write canceled state to DB immediately so other replicas see it. # The active asyncio task will receive CancelledError on its next await. await self.database.task_repository.update_state(task_id, "CANCELED") updater = TaskUpdater(event_queue, task_id, context.context_id) cancel_message = updater.new_agent_message( parts=[Part(root=TextPart(text="Response stopped."))], ) await updater.update_status( TaskState.canceled, message=cancel_message, final=True, ) async def execute(self, context, event_queue): try: # ... main agent loop ... except asyncio.CancelledError: # CancelledError propagates from run_stream() when the task is cancelled. # Don't emit any more events — the cancel() method already did. logger.info("Agent execution canceled") ``` ### Cross-Instance Cancellation In Kubernetes with multiple agent replicas, the cancel request may hit a different pod than the one running the task. Use a background polling watcher to detect cross-instance cancellation: ```python theme={null} async def _cancellation_watcher( self, task_id: str, parent_task: asyncio.Task ) -> None: """Poll DB every 5s for cancellation from another instance.""" while True: await asyncio.sleep(5) state = await self.database.task_repository.get_state(task_id) if state == "CANCELED": parent_task.cancel() return ``` Start the watcher at the beginning of `execute()` and cancel it in the `finally` block: ```python theme={null} cancel_watcher = asyncio.create_task( self._cancellation_watcher(task_id, asyncio.current_task()) ) try: # ... agent loop ... finally: cancel_watcher.cancel() ``` ## Reconnection Handling If an SSE connection drops mid-stream, the client sends a `tasks/resubscribe` request. The `DefaultRequestHandler` from the `a2a` library handles this automatically when used with a persistent task store. ```python theme={null} from a2a.server.tasks import InMemoryTaskStore # for development # Use a persistent task store for production — survives pod restarts task_store = PersistentTaskStore(database) # your database-backed implementation handler = DefaultRequestHandler( agent_executor=executor, task_store=task_store, ) ``` `InMemoryTaskStore` does not survive pod restarts. In production, implement a database-backed `TaskStore` (e.g., backed by PostgreSQL). If the task store loses state, clients that attempt `tasks/resubscribe` will receive a `TaskNotFoundError`. ## LLM Error Retry Transient LLM errors (HTTP 5xx, 429) should be retried with exponential backoff. A well-behaved executor retries up to 2 times: ```python theme={null} MAX_LLM_RETRIES = 2 LLM_RETRY_BACKOFF_BASE = 2.0 # seconds: 2, 4 for attempt in range(MAX_LLM_RETRIES + 1): try: async with self.agent.run_stream(...) as run: async for text in run.stream_text(): ... break # success — exit retry loop except ModelHTTPError as exc: if exc.status_code == 429 or exc.status_code >= 500: if attempt < MAX_LLM_RETRIES: delay = LLM_RETRY_BACKOFF_BASE * (2 ** attempt) await asyncio.sleep(delay) else: await self._handle_agent_failure(exc, response_message, updater) raise else: # 4xx non-429: not retryable await self._handle_agent_failure(exc, response_message, updater) raise ``` ## First-Token Latency Track time-to-first-token to detect model cold starts and MCP connection overhead: ```python theme={null} stream_start = time.monotonic() first_token_recorded = False async for text_content in run.stream_text(): if not first_token_recorded: first_token_latency = time.monotonic() - stream_start metrics.record_stream_first_token(first_token_latency) first_token_recorded = True # ... stream the chunk ``` Typical first-token latencies on CRAFT: 500ms–2s for Gemini Flash, 1s–4s for Gemini Pro with cold MCP connections. ## Next Steps Inspect streaming traces and diagnose SSE issues. Set up Langfuse to evaluate streaming agent behaviour. # Tool Authoring Source: https://docs.emergence.ai/guides/agent-author/tool-authoring How to write function tools, connect MCP tools via FastMCPToolset, and apply schema discipline across frameworks. # Tool Authoring Tools are the primary way agents access external capabilities — databases, APIs, file storage, computation environments. Getting tools right is critical: poorly specified tools are the leading cause of agent errors, context bloat, and cost overruns. ## Function Tools Function tools are Python functions that the LLM can call directly. Every supported framework converts them to the appropriate wire format for the underlying model. ### Writing a Good Tool A well-authored function tool has four properties: 1. **A descriptive docstring** — the LLM reads this to decide when to call the tool 2. **Typed parameters** — every parameter has an explicit type annotation and docstring description 3. **A bounded return type** — string, dict, or a typed Pydantic model; never untyped or variable-shape 4. **Minimal side effects** — each call does one thing; avoid tools that "do everything" ```python theme={null} def get_schema(table_name: str, schema_fqn: str) -> dict: """Return the column definitions for a database table. Call this before generating SQL to understand the table structure. Returns an empty dict if the table does not exist. Args: table_name: The simple table name (e.g. "orders"). schema_fqn: The fully qualified schema name (e.g. "analytics-db.analytics_db.public"). Returns: A dict with keys: columns (list of {name, type, nullable}), row_count (int). """ # ... implementation return {"columns": [...], "row_count": 1234} ``` ### Schema Discipline The tool's parameter schema is sent to the LLM on every call. Verbose or inaccurate schemas degrade routing quality and inflate cost. Prefer `Literal["postgres", "redshift", "bigquery"]` over `str` when the set of valid values is known. The LLM will hallucinate less when the schema constrains choices. ```python theme={null} from typing import Literal def execute_query( sql: str, dialect: Literal["postgres", "redshift", "bigquery"], ) -> dict: """Execute a SQL query against the target database.""" ... ``` Use `Optional[T]` with a sensible default. Never use `Union[T, None]` without a default — the LLM will try to supply a value when it shouldn't. ```python theme={null} from typing import Optional def search_artifacts( query: str, max_results: int = 10, artifact_type: Optional[Literal["parquet", "chart", "code"]] = None, ) -> list[dict]: """Search for artifacts matching the query.""" ... ``` Raise only for truly unrecoverable situations. For expected failures (table not found, invalid SQL), return a structured error dict so the LLM can understand and recover. ```python theme={null} def execute_sql(sql: str) -> dict: """Execute SQL and return results or a structured error.""" try: rows = db.execute(sql) return {"success": True, "rows": rows, "error": None} except QueryError as e: # Return the error — don't raise. The LLM will retry with corrected SQL. return {"success": False, "rows": [], "error": str(e)} ``` ## MCP Tools via FastMCPToolset CRAFT exposes platform capabilities through `em-runtime-mcp` — an MCP server. Pydantic AI connects to it via `FastMCPToolset`; Google ADK can connect via a `MCPToolset` wrapper. ### Pydantic AI — FastMCPToolset A production-grade pattern for connecting a Pydantic AI agent to the CRAFT MCP server: ```python theme={null} from pydantic_ai.toolsets.fastmcp import FastMCPToolset from fastmcp import Client from fastmcp.client.transports import StreamableHttpTransport # Create a fresh client and toolset per agent request. # Never reuse a long-lived client — MCP sessions expire. mcp_client = Client( transport=StreamableHttpTransport( url="https://craft.emergence.ai/mcp", headers={ "Authorization": "Bearer ", "X-Project-ID": "", }, ) ) toolset = FastMCPToolset(mcp_client) # Inject into the agent run async with agent.run_stream( user_message, deps=agent_deps, toolsets=[toolset], ) as run: async for text in run.stream_text(): ... # Always close the client after the run await mcp_client.close() ``` ### Google ADK — MCP Tool Integration Google ADK connects to the CRAFT MCP server using `StreamableHTTPConnectionParams`: ```python theme={null} from google.adk.tools.mcp_tool import MCPToolset, StreamableHTTPConnectionParams from google.adk.agents import LlmAgent toolset = MCPToolset( connection_params=StreamableHTTPConnectionParams( url="https://craft.emergence.ai/mcp", headers={ "Authorization": "Bearer ", "X-Project-ID": "", }, ) ) root_agent = LlmAgent( model="gemini-3.5-flash", name="my_agent", description="Agent with CRAFT MCP tool access", instruction="Use available MCP tools to answer questions.", tools=[toolset], ) ``` The older import `from google.adk.toolsets.mcp import MCPToolset` is deprecated. Use `from google.adk.tools.mcp_tool import MCPToolset, StreamableHTTPConnectionParams` (ADK v1.0+). ### Claude Agent SDK — MCP Tool Integration The Anthropic API's `mcp_servers` connector only supports `authorization_token` — it cannot pass `X-Project-ID` as a separate header. Use the `mcp` Python SDK with a session-aware client to call CRAFT tools from a standard function-tool loop: ```python theme={null} import asyncio import anthropic from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client CRAFT_URL = "https://craft.emergence.ai/mcp" CRAFT_HEADERS = { "Authorization": "Bearer ", "X-Project-ID": "", } async def run_craft_agent(user_question: str) -> str: client = anthropic.Anthropic() async with streamablehttp_client(CRAFT_URL, headers=CRAFT_HEADERS) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() tools_result = await session.list_tools() tools = [ { "name": t.name, "description": t.description or "", "input_schema": t.inputSchema, } for t in tools_result.tools ] messages = [{"role": "user", "content": user_question}] while True: response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages, ) if response.stop_reason == "end_turn": return response.content[0].text tool_results = [] for block in response.content: if block.type == "tool_use": result = await session.call_tool(block.name, block.input) text = result.content[0].text if result.content else "" tool_results.append( {"type": "tool_result", "tool_use_id": block.id, "content": text} ) messages += [ {"role": "assistant", "content": response.content}, {"role": "user", "content": tool_results}, ] ``` ### LangGraph — MCP Tool Integration LangGraph connects to CRAFT via `MultiServerMCPClient` from `langchain-mcp-adapters` (install: `pip install langchain-mcp-adapters`): ```python theme={null} from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.prebuilt import create_react_agent async def build_craft_agent(): async with MultiServerMCPClient({ "craft": { "url": "https://craft.emergence.ai/mcp", "transport": "http", # streamable-HTTP (MCP 2025-11-25); "sse" is the legacy transport "headers": { "Authorization": "Bearer ", "X-Project-ID": "", }, } }) as mcp_client: tools = await mcp_client.get_tools() agent = create_react_agent("anthropic:claude-opus-4-8", tools) result = await agent.ainvoke( {"messages": [{"role": "user", "content": "List my data connections"}]} ) return result ``` `langchain-mcp-adapters` supports `headers` for static auth values. If the access token expires mid-session, reinitialise the client — dynamic per-request token injection is a [known limitation](https://github.com/langchain-ai/langchain-mcp-adapters/issues/194). ## Cross-Framework Tool Comparison | Feature | Google ADK | Claude Agent SDK | Pydantic AI | LangGraph | | --------------------- | ----------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------- | | Function tools | `tools=[my_fn]` | `tools=[{name, description, input_schema}]` | `@agent.tool` decorator or `tools=[my_fn]` | `bind_tools([my_fn])` | | CRAFT MCP connection | `MCPToolset` + `StreamableHTTPConnectionParams` | `mcp` SDK `streamablehttp_client` + function tools | `FastMCPToolset` + `StreamableHttpTransport` (first-class) | `MultiServerMCPClient` (`transport="http"`) | | Parallel tool calls | Yes (automatic) | Yes (with `betas=["interleaved-thinking"]`) | `parallel_tool_calls=True` in `ModelSettings` | Node-level parallelism | | Tool retry on failure | ADK handles automatically | Manual in tool loop | `ModelRetry` exception | Conditional edges | | Context injection | `ToolContext` | Manual in tool dispatch | `RunContext[Deps]` | `RunnableConfig` | ## Limiting Iteration and Cost Unbounded tool call loops are the leading cause of cost overruns. Implement iteration limits defensively. ```python theme={null} # Pydantic AI — track failures on deps if not hasattr(ctx.deps, "_code_failure_count"): ctx.deps._code_failure_count = 0 ctx.deps._code_failure_count += 1 if ctx.deps._code_failure_count >= settings.max_code_failures: # Return a structured error instead of raising ModelRetry. # Raising ModelRetry here exhausts the retry budget and raises # UnexpectedModelBehavior — losing the user-facing message. return { "success": False, "error": ( f"Execution has failed {ctx.deps._code_failure_count} times. " "Ask the user for clarification." ), } ``` Return structured error dicts (not `raise ModelRetry`) when you have hit an iteration limit. `ModelRetry` with the default `max_retries=1` already consumed will raise `UnexpectedModelBehavior` and produce a generic failure message instead of the clarifying ask you wanted the model to emit. ## Pre-Flight Validation Validate tool inputs before making expensive network calls. A `FastMCPToolset` subclass can run a lint pass before code execution: ```python theme={null} async def call_tool(self, name, tool_args, ctx, tool): if name == "run_code": code = tool_args.get("code", "") violations = lint_code(code) if violations: # Reject before the sandbox round-trip. # Saves latency and gives the model a sharper error message. ctx.deps._code_failure_count += 1 return { "success": False, "error": f"Forbidden pattern(s): {', '.join(v.pattern for v in violations)}", } # ... proceed to actual tool call ``` ## Next Steps Use tools and sub-agents together in orchestrated workflows. Inspect tool call traces and diagnose tool failures. # Your First CRAFT Agent Source: https://docs.emergence.ai/guides/agent-author/your-first-agent Build a working CRAFT-integrated agent in three incremental steps — from a bare hello-world to a registered, tool-calling platform agent — in under 20 minutes. This tutorial builds the same agent three times — once for each capability layer — so you can stop at whichever level you need. By the end you have a registered CRAFT agent that lists your project's data connections and is discoverable by other platform services. **What you'll build:** A data-assistant agent that: 1. Responds to prompts using CRAFT's managed LLM gateway 2. Calls the CRAFT Assets API to list real data connections 3. Is registered in the CRAFT agent registry **Time:** \~5 min per step. Steps are independent — come back later for the next one. **Prerequisites:** Obtain a CRAFT project access token via your deployment's OIDC token endpoint (client credentials grant) and export the connection settings: ```bash theme={null} export CRAFT_TOKEN="$(curl -s -X POST "${OIDC_TOKEN_URL}" \ -d "grant_type=client_credentials&client_id=${OIDC_CLIENT_ID}&client_secret=${OIDC_CLIENT_SECRET}" \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')" export CRAFT_GATEWAY_URL="" # LiteLLM sidecar or gateway export CRAFT_ASSETS_URL="" # CRAFT Assets API export CRAFT_PROJECT_ID="" ``` For local dev, run `docker-compose up` from the solution starter template — it pre-configures `OIDC_TOKEN_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and the local URLs. *** ## Step 1 — Connect to CRAFT's LLM gateway The only difference between a standalone agent and a CRAFT-native agent is where it routes its LLM calls. CRAFT runs a [LiteLLM gateway](/platform/llm-gateway) that handles model selection, project billing, and rate limits. All four frameworks reach it through an OpenAI-compatible endpoint. Pick your framework: ```bash theme={null} pip install google-adk litellm ``` ```python agent.py theme={null} import os from google.adk.agents import Agent from google.adk.models.lite_llm import LiteLlm from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types # Point ADK at CRAFT's LiteLLM gateway via the LiteLlm wrapper. # The "openai/" prefix tells LiteLLM to use the OpenAI-compatible endpoint. model = LiteLlm( model="openai/claude-opus-4-8", api_base=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) agent = Agent( name="data_assistant", model=model, description="Helps users understand the data available in their CRAFT project.", instruction="You are a helpful data assistant. Answer questions about the user's data concisely.", ) async def main(): session_service = InMemorySessionService() session = await session_service.create_session( app_name="data_assistant", user_id="dev", session_id="s1" ) runner = Runner(agent=agent, app_name="data_assistant", session_service=session_service) async for event in runner.run_async( user_id="dev", session_id="s1", new_message=types.Content(role="user", parts=[types.Part(text="What can you help me with?")]), ): if event.is_final_response(): print(event.content.parts[0].text) if __name__ == "__main__": import asyncio asyncio.run(main()) ``` For production multi-agent patterns with A2A, see the [multi-agent patterns guide](/guides/agent-author/multi-agent-patterns) — it shows how an orchestrator uses ADK to chain a text-to-SQL agent and an insights agent over the A2A protocol. ```bash theme={null} pip install anthropic openai ``` ```python agent.py theme={null} import os from openai import OpenAI # Route through CRAFT's LiteLLM gateway using the OpenAI-compatible client. # LiteLLM serves claude-opus-4-8 (and other models) at the standard # /v1/chat/completions endpoint — no Anthropic account needed. client = OpenAI( base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) def run(prompt: str) -> str: response = client.chat.completions.create( model="claude-opus-4-8", max_tokens=1024, messages=[ {"role": "system", "content": "You are a helpful data assistant. Answer questions about the user's data concisely."}, {"role": "user", "content": prompt}, ], ) return response.choices[0].message.content if __name__ == "__main__": print(run("What can you help me with?")) ``` The OpenAI client talks to CRAFT's LiteLLM gateway using Claude via the OpenAI-compatible endpoint. LiteLLM translates function-calling requests to the format each model expects, so the same pattern works for any model in the CRAFT allowlist. ```bash theme={null} pip install pydantic-ai ``` ```python agent.py theme={null} import os from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider # Route through CRAFT's LiteLLM gateway using the OpenAI-compatible provider. model = OpenAIChatModel( "claude-opus-4-8", provider=OpenAIProvider( base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ), ) agent = Agent( model, system_prompt="You are a helpful data assistant. Answer questions about the user's data concisely.", ) result = agent.run_sync("What can you help me with?") print(result.output) ``` Pydantic AI's `FastMCPToolset` wires MCP servers as tools with first-class typing. See the [tool authoring guide](/guides/agent-author/tool-authoring) for that pattern. ```bash theme={null} pip install langgraph langchain-openai ``` ```python agent.py theme={null} import os from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent from langchain_core.messages import HumanMessage # Route through CRAFT's LiteLLM gateway — it's OpenAI-compatible. llm = ChatOpenAI( model="claude-opus-4-8", base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) agent = create_react_agent( model=llm, tools=[], # add tools in Step 2 prompt="You are a helpful data assistant. Answer questions about the user's data concisely.", ) result = agent.invoke({"messages": [HumanMessage(content="What can you help me with?")]}) print(result["messages"][-1].content) ``` That's it. Your agent is running on CRAFT's managed LLM infrastructure. ### Routing to specific backends The CRAFT gateway uses LiteLLM under the hood, so any model string it recognises works in the `model=` field — you don't need to change any client code. Ask your platform team which models are in the project's allowlist. ```python theme={null} # Model string format: vertex_ai/ # LiteLLM reads VERTEXAI_PROJECT + VERTEXAI_LOCATION (or the Google Cloud # standards GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION) and authenticates # via Workload Identity or GOOGLE_APPLICATION_CREDENTIALS automatically. # DEFAULT — fastest, highest quality on agentic + coding benchmarks model = "vertex_ai/gemini-3.5-flash" # SPECIALIZED — only if your agent depends on extensive custom tool calling # (the model prioritizes your registered functions over bash fallbacks) model = "vertex_ai/gemini-3.1-pro-preview-customtools" # SECONDARY — demonstrates model agnosticity model = "vertex_ai/claude-opus-4-8" ``` **Use `gemini-3.1-pro-preview-customtools` only when extensive custom tool calling is core to your agent.** Same intelligence as base `gemini-3.1-pro`, fine-tuned to prioritize registered custom functions over bash fallbacks. Google's own guidance: if >50% of requests don't involve tool calling, stay on `gemini-3.5-flash` — `3.5-flash` already beats `3.1-pro` on agentic benchmarks (MCP Atlas +5.4%) and coding (Terminal-Bench +6%) at 3.6× the speed ([source](https://wavespeed.ai/blog/posts/gemini-3-5-pro-flash/)), and the customtools variant degrades quality on non-tool workloads ([source](https://help.apiyi.com/en/gemini-3-1-pro-preview-customtools-agent-guide-en.html)). When you do use it: preview SLA, global endpoint only (`vertex_location` must be `global`), lower quota than GA — configure a fallback to `gemini-3.5-flash` or `claude-opus-4-8`. ```python theme={null} # Model string format: bedrock/ # LiteLLM uses AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY or instance profile. model = "bedrock/anthropic.claude-opus-4-8-20260301-v1:0" ``` ```python theme={null} # Model string format: azure/ # LiteLLM reads AZURE_API_KEY and AZURE_API_BASE from the gateway config. model = "azure/gpt-5.5" ``` ```python theme={null} # Nebius exposes an OpenAI-compatible endpoint; prefix with openai/. # LiteLLM reads NEBIUS_API_KEY from the gateway config. model = "openai/Qwen/Qwen3-30B-A3B" ``` *** ## Step 2 — Add a CRAFT tool Tools let your agent take action. The simplest CRAFT tool calls the Assets API to list the data connections registered in your project — real databases and warehouses the platform knows about. ```python agent.py theme={null} import os import httpx from google.adk.agents import Agent from google.adk.models.lite_llm import LiteLlm from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types # ── CRAFT tool ───────────────────────────────────────────────────────── def list_data_connections() -> str: """List the data connections available in the current CRAFT project.""" resp = httpx.get( f"{os.environ['CRAFT_ASSETS_URL']}/assets/data", headers={ "Authorization": f"Bearer {os.environ['CRAFT_TOKEN']}", "X-Project-ID": os.environ["CRAFT_PROJECT_ID"], }, ) resp.raise_for_status() items = resp.json().get("data", []) if not items: return "No data connections found in this project." return "\n".join(f"- {c['name']} ({c['connection_type']})" for c in items) # ── Agent ────────────────────────────────────────────────────────────── model = LiteLlm( model="openai/claude-opus-4-8", api_base=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) agent = Agent( name="data_assistant", model=model, description="Helps users understand the data available in their CRAFT project.", instruction="You are a helpful data assistant. Use list_data_connections to answer questions about available data.", tools=[list_data_connections], ) async def main(): session_service = InMemorySessionService() session = await session_service.create_session( app_name="data_assistant", user_id="dev", session_id="s1" ) runner = Runner(agent=agent, app_name="data_assistant", session_service=session_service) async for event in runner.run_async( user_id="dev", session_id="s1", new_message=types.Content(role="user", parts=[types.Part(text="What data connections do I have?")]), ): if event.is_final_response(): print(event.content.parts[0].text) if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ```python agent.py theme={null} import os import json import httpx from openai import OpenAI # Same CRAFT gateway client as Step 1 — tool calling works over the # OpenAI-compatible endpoint via LiteLLM's function-calling support. client = OpenAI( base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) # ── CRAFT tool definition (OpenAI function-calling format) ────────────── TOOLS = [ { "type": "function", "function": { "name": "list_data_connections", "description": ( "List all data connections (databases, data warehouses, APIs) registered " "in the current CRAFT project. Call this whenever the user asks what data " "is available." ), "parameters": {"type": "object", "properties": {}, "required": []}, }, } ] def list_data_connections() -> str: resp = httpx.get( f"{os.environ['CRAFT_ASSETS_URL']}/assets/data", headers={ "Authorization": f"Bearer {os.environ['CRAFT_TOKEN']}", "X-Project-ID": os.environ["CRAFT_PROJECT_ID"], }, ) resp.raise_for_status() items = resp.json().get("data", []) if not items: return "No data connections found in this project." return "\n".join(f"- {c['name']} ({c['connection_type']})" for c in items) TOOL_HANDLERS = {"list_data_connections": list_data_connections} # ── Agentic loop ──────────────────────────────────────────────────────── def run(prompt: str) -> str: messages = [ {"role": "system", "content": "You are a helpful data assistant. Use your tools to answer questions about available data."}, {"role": "user", "content": prompt}, ] while True: response = client.chat.completions.create( model="claude-opus-4-8", max_tokens=1024, tools=TOOLS, messages=messages, ) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content # Execute tool calls and feed results back results = [] for tc in msg.tool_calls: output = TOOL_HANDLERS[tc.function.name](**json.loads(tc.function.arguments or "{}")) results.append({ "role": "tool", "tool_call_id": tc.id, "content": output, }) messages.extend(results) if __name__ == "__main__": print(run("What data connections do I have?")) ``` ```python agent.py theme={null} import os import httpx from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider model = OpenAIChatModel( "claude-opus-4-8", provider=OpenAIProvider( base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ), ) agent = Agent( model, system_prompt="You are a helpful data assistant. Use your tools to answer questions about available data.", ) # ── CRAFT tool ───────────────────────────────────────────────────────── @agent.tool_plain def list_data_connections() -> str: """List all data connections registered in the current CRAFT project.""" resp = httpx.get( f"{os.environ['CRAFT_ASSETS_URL']}/assets/data", headers={ "Authorization": f"Bearer {os.environ['CRAFT_TOKEN']}", "X-Project-ID": os.environ["CRAFT_PROJECT_ID"], }, ) resp.raise_for_status() items = resp.json().get("data", []) if not items: return "No data connections found in this project." return "\n".join(f"- {c['name']} ({c['connection_type']})" for c in items) result = agent.run_sync("What data connections do I have?") print(result.output) ``` `@agent.tool_plain` registers the function as a tool with no dependency injection. Pydantic AI infers the JSON schema from the docstring and type hints automatically. ```python agent.py theme={null} import os import httpx from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.messages import HumanMessage from langgraph.prebuilt import create_react_agent llm = ChatOpenAI( model="claude-opus-4-8", base_url=os.environ["CRAFT_GATEWAY_URL"], api_key=os.environ["CRAFT_TOKEN"], ) # ── CRAFT tool ───────────────────────────────────────────────────────── @tool def list_data_connections() -> str: """List all data connections registered in the current CRAFT project. Call this whenever the user asks what data sources or databases are available. """ resp = httpx.get( f"{os.environ['CRAFT_ASSETS_URL']}/assets/data", headers={ "Authorization": f"Bearer {os.environ['CRAFT_TOKEN']}", "X-Project-ID": os.environ["CRAFT_PROJECT_ID"], }, ) resp.raise_for_status() items = resp.json().get("data", []) if not items: return "No data connections found in this project." return "\n".join(f"- {c['name']} ({c['connection_type']})" for c in items) agent = create_react_agent( model=llm, tools=[list_data_connections], prompt="You are a helpful data assistant. Use your tools to answer questions about available data.", ) result = agent.invoke({"messages": [HumanMessage(content="What data connections do I have?")]}) print(result["messages"][-1].content) ``` Your agent now answers questions using real platform data. Every other CRAFT capability — agents, files, models, artifacts — is one more `httpx.get()` away. *** ## Step 3 — Register your agent A registered agent is discoverable by other platform services, the UI, and other agents via the A2A protocol. Registration is one `POST` to the Assets API. ```python register.py theme={null} import os import httpx # POST /assets/agents expects the full Agent Card wrapped in "agent_card". # "name" and "version" are required; "url" points to your running service's # /.well-known/agent-card.json endpoint so other agents can fetch full details. registration = { "agent_card": { "name": "data-assistant", "version": "1.0.0", "description": "Answers questions about data connections in a CRAFT project.", "url": "http://your-service-host/.well-known/agent-card.json", "capabilities": { "streaming": False, "push_notifications": False, }, }, "tags": ["data", "assistant"], } resp = httpx.post( f"{os.environ['CRAFT_ASSETS_URL']}/assets/agents", json=registration, headers={ "Authorization": f"Bearer {os.environ['CRAFT_TOKEN']}", "X-Project-ID": os.environ["CRAFT_PROJECT_ID"], }, ) resp.raise_for_status() agent_id = resp.json()["resource_uri"] print(f"Registered: {agent_id}") ``` The `agent_card_url` points to a `/.well-known/agent-card.json` endpoint your service exposes — it describes your agent's skills so other agents can call it. See the [A2A protocol primer](/guides/agent-author/a2a-protocol-primer) for the Agent Card schema, and [multi-agent patterns](/guides/agent-author/multi-agent-patterns) for wiring A2A delegation. Re-POSTing the same `name` in a project returns **409 `RESOURCE_ALREADY_EXISTS`**. To update an existing registration, `PUT /assets/agents/{resource_uri}` and include the current ETag in `If-Match`: * `PUT` without `If-Match` returns **428 `Precondition Required`**. * `PUT` with a stale `If-Match` returns **412 `Precondition Failed`** (`"ETag mismatch — resource was modified"`). Read the current ETag from the `ETag` header on a prior `GET /assets/agents/{resource_uri}` (or use the `current_version` field from the JSON body — both are equivalent), and feed it back on the next `PUT`. Either the bare integer (`If-Match: 1`) or the quoted form (`If-Match: "1"`) is accepted; weak ETags (`W/"1"`) are rejected. Automate the `GET → PUT(If-Match)` cycle in your deploy pipeline so the registry stays in sync with your running service. *** ## What's next Function tools, MCP tools via FastMCPToolset, schema discipline. A2A delegation, parallel fan-out, supervisor agents. Agent Cards, JSON-RPC over SSE, task lifecycle. Golden traces, Langfuse evaluators, regression suites. # Backup & Restore Source: https://docs.emergence.ai/guides/backup-restore How to back up and restore CRAFT data including PostgreSQL databases, Keycloak realms, OpenFGA state, and Infisical secrets. # Backup and Restore Platform Data This guide covers backup and restore procedures for all stateful components of CRAFT. Each service owns its own database, and infrastructure services (Keycloak, OpenFGA, Infisical) maintain independent state that must be backed up separately. ## Stateful Components The platform has the following stateful components that require backup: | Component | Storage | Data | | ----------------- | --------------- | ------------------------------------------------- | | **Governance DB** | PostgreSQL | Organizations, projects, users, role assignments | | **Assets DB** | PostgreSQL | Artifacts, data connections, files, models | | **Utils DB** | PostgreSQL | Data catalog, scheduling, context packs, memories | | **Keycloak** | PostgreSQL | Realms, users, IdP configurations, sessions | | **OpenFGA** | PostgreSQL | Authorization model, relationship tuples | | **Infisical** | Internal store | Application secrets, data connection credentials | | **Redis** | In-memory + AOF | Cache, session state, event streams | | **Solution DBs** | PostgreSQL | Data Insights sessions, Data Governance profiles | Each service has a separate PostgreSQL database. There are no cross-service foreign keys, so databases can be backed up independently without coordination. ## PostgreSQL Backup ### Automated Backups (Cloud-Managed) For cloud-managed PostgreSQL (Cloud SQL, RDS, Azure Database): Cloud SQL provides automated daily backups with configurable retention. ```bash theme={null} # Enable automated backups gcloud sql instances patch \ --backup-start-time=02:00 \ --retained-backups-count=30 \ --retained-transaction-log-days=7 # Create an on-demand backup gcloud sql backups create --instance= # List available backups gcloud sql backups list --instance= ``` ```bash theme={null} # Enable automated backups with 30-day retention aws rds modify-db-instance \ --db-instance-identifier \ --backup-retention-period 30 # Create a manual snapshot aws rds create-db-snapshot \ --db-instance-identifier \ --db-snapshot-identifier emergence-backup-$(date +%Y%m%d) ``` ```bash theme={null} # Azure Database for PostgreSQL has automated backups enabled by default # Configure retention (7-35 days) az postgres flexible-server update \ --resource-group \ --name \ --backup-retention 30 ``` ### Manual Backups (On-Premises) For self-managed PostgreSQL deployments: ```bash theme={null} # Back up all platform databases DATABASES="keycloak openfga infisical governance assets utils prefect datareadiness talk2data" BACKUP_DIR="/backups/emergence/$(date +%Y%m%d_%H%M%S)" mkdir -p "$BACKUP_DIR" for db in $DATABASES; do pg_dump -h localhost -U postgres -Fc -f "$BACKUP_DIR/$db.dump" "$db" done # Verify backups for dump in "$BACKUP_DIR"/*.dump; do pg_restore --list "$dump" > /dev/null && echo "OK: $dump" || echo "FAILED: $dump" done ``` Use `pg_dump -Fc` (custom format) for backups. It supports parallel restore and selective table restoration, and compresses data automatically. ### Point-in-Time Recovery For production deployments, enable WAL archiving for point-in-time recovery: ```bash theme={null} # postgresql.conf archive_mode = on archive_command = 'cp %p /archive/wal/%f' wal_level = replica ``` ## Keycloak Backup Keycloak state is primarily stored in its PostgreSQL database, but realm configuration can also be exported as JSON for version control. ```bash theme={null} # Export realm configuration (includes users, IdPs, clients) # Run from within the Keycloak container or pod /opt/keycloak/bin/kc.sh export \ --dir /tmp/keycloak-export \ --realm \ --users realm_file # Copy the export out of the container kubectl cp :/tmp/keycloak-export ./keycloak-backup/ ``` Realm exports include user accounts but not user credentials (passwords). Users will need to reset passwords after a realm import. SSO users are unaffected since their credentials are managed by the external IdP. ## OpenFGA Backup OpenFGA stores its authorization model and relationship tuples in PostgreSQL. The database backup covers all OpenFGA state. For additional safety, export the authorization model: ```bash theme={null} # Export the current authorization model curl -s "http://:8080/stores//authorization-models" \ -H "Authorization: Bearer $TOKEN" | jq . > openfga-model-backup.json ``` ## Secrets Backend Backup The platform supports two secrets backends. Back up whichever you are using. Infisical manages application secrets including data connection credentials. Back up the Infisical database (PostgreSQL `infisical` database) and preserve the encryption keys. Infisical secrets are encrypted at rest. Backing up the database preserves the encrypted data, but you must also preserve the `ENCRYPTION_KEY` and `AUTH_SECRET` for restoration, without them, encrypted data cannot be decrypted. ```bash theme={null} pg_dump -h localhost -U postgres -Fc -f infisical-backup.dump infisical # Also export the encryption key from the infisical-bootstrap-secret: kubectl get secret infisical-bootstrap-secret -n em-runtime -o yaml > infisical-bootstrap-secret.yaml ``` Secrets stored in GCP Secret Manager have built-in version history. No separate backup action is required, GCP retains all secret versions. To export a snapshot of all current secret values: ```bash theme={null} # List all em-runtime secrets gcloud secrets list --project=your-project-id --filter="name:em-runtime" # Verify each secret has at least one active version gcloud secrets versions list em-runtime --project=your-project-id ``` For disaster recovery, ensure IAM bindings for the ESO service account are documented and reproducible via Terraform. ## Redis Backup Redis serves as a cache and event stream. While cache data is ephemeral, you may want to back up Redis for faster recovery: ```bash theme={null} # Trigger a manual RDB snapshot redis-cli -a $REDIS_PASSWORD BGSAVE # Copy the dump file kubectl cp :/data/dump.rdb ./redis-backup/dump.rdb ``` ## Restore Procedures ### Restore PostgreSQL Databases ```bash theme={null} # Restore a single database from a custom-format dump pg_restore -h localhost -U postgres -d governance -Fc --clean --if-exists governance.dump # Restore all platform databases for db in keycloak openfga infisical governance assets utils prefect datareadiness talk2data; do pg_restore -h localhost -U postgres -d "$db" -Fc --clean --if-exists "$db.dump" done ``` ### Restore Keycloak Realms ```bash theme={null} # Import realm configuration /opt/keycloak/bin/kc.sh import \ --dir /tmp/keycloak-import \ --override true ``` ### Restore from Cloud Managed Backups ```bash theme={null} # Restore from an automated backup gcloud sql backups restore \ --restore-instance= ``` ```bash theme={null} # Restore from a snapshot (creates a new instance) aws rds restore-db-instance-from-db-snapshot \ --db-instance-identifier \ --db-snapshot-identifier ``` ## Backup Schedule Recommendations | Component | Frequency | Retention | Method | | --------------------------- | ---------------------- | ---------- | ------------------------------------------------------------ | | PostgreSQL (all DBs) | Daily + continuous WAL | 30 days | Cloud-managed or pg\_dump | | Keycloak realm exports | Weekly | 90 days | JSON export | | OpenFGA model | On change | Indefinite | JSON export in version control | | Redis | Daily | 7 days | RDB snapshot | | Secrets backend (Infisical) | Daily | 30 days | Database backup with encryption keys | | Secrets backend (GCP SM) | N/A, managed | Indefinite | GCP retains all versions; document IAM bindings in Terraform | ## Disaster Recovery Checklist Restore all platform databases from the latest backup. Verify row counts and data integrity. Import realm configurations. SSO users will re-authenticate via their IdP. Local users may need password resets. Confirm the authorization model is loaded. The Governance service re-applies the schema on startup. **Infisical**: Restore the Infisical database and encryption keys. Verify data connection credentials are accessible. **ESO + GCP Secret Manager**: Verify GCP Secret Manager secrets are intact and ESO ClusterSecretStore can authenticate via Workload Identity. Follow the startup order: PostgreSQL -> Redis -> Keycloak -> OpenFGA -> Governance -> Assets/Utils -> Solutions. Run health checks on all services. Test authentication, permission checks, and data connection queries. ## Next Steps Review the full deployment architecture and infrastructure requirements. Configure Helm values for backup-related settings. Understand data retention requirements for GDPR compliance. Secure backup data in transit and at rest. # Audit Log Source: https://docs.emergence.ai/guides/customer-admin/audit-log View and export the audit log for your CRAFT organisation — what events are captured and how to use them for compliance. The audit log is a tamper-evident record of significant actions taken by users and the platform within your organisation. It is available to organisation owners and admins and is scoped strictly to your organisation — you cannot see events from other organisations. ## Where to find the audit log In the CRAFT platform, navigate to **Settings** and select **Audit Log** from the sidebar. ## What the audit log captures The audit log records actions across the following categories: | Category | Examples of events captured | | ---------------------- | -------------------------------------------------------------------------------------- | | **User management** | User invited, role changed, account deactivated, account reactivated | | **Project management** | Project created, project deleted, member added to project, member removed from project | | **Data connections** | Data connection created, updated, or deleted | | **Secrets** | Secret created, secret deleted (content is never logged) | | **Authentication** | Login, logout, failed login attempt | | **Agent activity** | Agent tool calls (when agent audit is enabled for a project) | | **Audit settings** | Retention period changed | Each event record includes: | Field | Description | | ------------- | ------------------------------------------------------------ | | **Timestamp** | When the event occurred (UTC) | | **Actor** | The user or service that performed the action | | **Action** | What was done (for example, `user.invite`, `project.create`) | | **Resource** | The specific resource that was affected | | **Project** | The project context, if applicable | | **Details** | Additional context specific to the event type | ## Viewing and filtering the audit log Navigate to **Settings** and select **Audit Log**. Use the filter options to narrow down the events you want to review: * **Date range**: select a start and end date * **Actor**: filter by a specific user * **Action**: filter by action type (for example, show only login events) * **Resource type**: filter by the type of resource affected * **Project**: filter to events within a specific project Filters can be combined. For example, you can see all actions taken by a specific user within a date range. The list shows matching events in reverse chronological order (newest first). Select any event to expand it and see the full details. ## Exporting the audit log You can export audit events to a CSV file for use in compliance reports, external monitoring tools, or archiving. Set the date range and any other filters for the events you want to export. Select **Export to CSV** in the top-right area of the Audit Log page. The file will download to your browser. Exports are paginated at up to 50,000 rows per file. If your date range contains more than 50,000 events, export in smaller date ranges and combine the files. ## Audit log retention By default, audit events are retained for 90 days. Organisation owners and admins can adjust the retention period under **Settings → Audit Log → Retention Settings**. Reducing the retention period will immediately delete events outside the new window. This action cannot be undone. Set a retention period that meets your compliance requirements before reducing it. ## Compliance use cases The audit log is designed to support common compliance and security review scenarios: **Access reviews:** Filter by date range to see which users had access to which resources during a specific period. Look for role change events to identify when permissions were granted or removed. **Incident investigation:** Filter by actor and date range to reconstruct the sequence of actions taken by a specific user during an incident window. **Data access auditing:** Filter by resource type `data_connection` and action `execute` to see which users ran queries against a specific data source. **Offboarding verification:** After deactivating a user, search for events by their user ID to confirm their access was removed and identify any resources that may need a new owner. # Configure SSO Source: https://docs.emergence.ai/guides/customer-admin/configure-sso Set up single sign-on for your organisation in CRAFT using your existing identity provider. Single sign-on (SSO) lets your team log in to CRAFT using their existing company credentials. Once SSO is configured, users are redirected to your identity provider when they access CRAFT — they do not need to set or remember a separate CRAFT password. SSO configuration requires organisation owner permissions. If you do not see the SSO settings described below, contact your organisation owner. ## Supported identity providers CRAFT supports SSO via two standard protocols: | Protocol | Compatible providers | | ------------------------- | ----------------------------------------------------------------------------------------------- | | **OIDC** (OpenID Connect) | Microsoft Entra ID (formerly Azure AD), Google Workspace, Okta, and any OIDC-compliant provider | | **SAML 2.0** | Microsoft Entra ID, Okta, Ping Identity, and any SAML 2.0-compliant provider | If your provider is not listed, contact Emergence support — CRAFT can be configured to work with any standards-compliant identity provider. ## Before you begin You will need the following information from your identity provider administrator: **For OIDC:** * Discovery URL (also called the "well-known" endpoint), in the format `https://your-provider.example.com/.well-known/openid-configuration` * Client ID and client secret (you will create an application in your identity provider and receive these) * Redirect URL to enter in your identity provider: provided in the CRAFT SSO settings screen **For SAML:** * Identity provider metadata URL or XML file * Redirect URL and SP entity ID to enter in your identity provider: provided in the CRAFT SSO settings screen ## Configure SSO in CRAFT In CRAFT, navigate to **Settings** and select **Single Sign-On** from the sidebar. Choose **OIDC** or **SAML** depending on what your identity provider supports. If both are available, OIDC is recommended for simplicity. Fill in the connection details for your provider: * **For OIDC**: enter the discovery URL, client ID, and client secret. * **For SAML**: upload the metadata XML file or enter the metadata URL. CRAFT will show you the redirect URL and any other values you need to enter in your identity provider's application configuration. In your identity provider's administration console, create an application or enterprise app for CRAFT and enter the values shown in the CRAFT SSO settings screen (redirect URL, entity ID, etc.). Refer to your identity provider's documentation for the specific steps: * [Microsoft Entra ID — OIDC app registration](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) * [Google Workspace — OIDC setup](https://developers.google.com/identity/openid-connect/openid-connect) * [Okta — OIDC app setup](https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/) For the engineer-side configuration details and Helm chart settings, see [SSO Integration Guide](/guides/sso-integration). After saving your configuration, select **Test Connection**. CRAFT will attempt to authenticate with your identity provider. If the test succeeds, SSO is ready to use. Test with your own account first before asking other users to switch to SSO login. This way you can confirm the configuration is correct without affecting your team. Once the connection test passes, toggle **SSO Enabled** to on. From this point, users will be redirected to your identity provider when they log in to CRAFT. ## What happens to existing users When you enable SSO, existing users who log in with a CRAFT password are asked to re-authenticate via your identity provider. Their email address in CRAFT must match the email address in your identity provider — otherwise they will be treated as a new user. If a user's email address in CRAFT does not match their identity provider email, contact Emergence support to merge the accounts. ## Troubleshooting The user's email address may not be in the group or application you configured in your identity provider. Check that the user has been assigned to the CRAFT application in your identity provider's admin console. Check that the redirect URL in your identity provider exactly matches the value shown in the CRAFT SSO settings screen. A trailing slash or incorrect protocol (http vs https) will cause redirect failures. Double-check the client ID and client secret. If you recently rotated the client secret in your identity provider, you will need to update it in the CRAFT SSO settings. # Manage Users Source: https://docs.emergence.ai/guides/customer-admin/manage-users Invite users to your organisation, assign roles, and deactivate accounts in CRAFT. User management is available to organisation owners and admins. You can invite new users, assign or change their roles, and deactivate accounts that should no longer have access. ## Where to find user management In the CRAFT platform, go to **Settings** in the main navigation, then select **Members** or **Users** from the settings sidebar. ## Invite a new user Navigate to **Settings** and select **Members**. You will see a list of all current members in your organisation. Select the **Invite Member** button in the top-right area of the Members page. Type the email address of the person you want to invite. Make sure you use the same email address they use with your company's identity provider if SSO is configured — otherwise they may have trouble logging in. Choose the role that matches what this person needs to do. See [Roles and Permissions](/guides/customer-admin/roles-and-permissions) for a description of each role. You can change the role at any time after the user accepts their invitation. Select **Send Invite**. The user will receive an email with a link to accept the invitation and set up their account. If the user does not receive the invitation email within a few minutes, ask them to check their spam folder. If SSO is configured, they may be directed to log in with their company credentials instead of setting a password. ## Change a user's role On the **Members** page, locate the user whose role you want to change. You can use the search field to find them by name or email address. Select the user's name or the options menu (three dots) next to their row. Select **Edit Role** or the current role name and choose a new role from the dropdown. The change takes effect immediately — the user does not need to log out and back in. ## Deactivate an account When someone leaves your organisation or no longer needs access, deactivate their account rather than deleting it. Deactivation preserves any work they created while removing their ability to log in. On the **Members** page, locate the user you want to deactivate. Select the options menu (three dots) next to the user's row and choose **Deactivate**. Confirm the action when prompted. The user will appear as **Inactive** on the Members page. They will not be able to log in, and their API keys (if any) will be revoked immediately. Deactivating a user does not delete their work. Projects, data connections, and other resources they created remain in place. If you need to transfer ownership of a resource, reassign it to another user before deactivating the account. ## Reactivate a deactivated account If someone needs access again after being deactivated, select the options menu next to their row on the Members page and choose **Reactivate**. You will be prompted to confirm and re-select their role. # Customer Admin Guide Source: https://docs.emergence.ai/guides/customer-admin/overview What CRAFT org admins can do — and what they cannot — with quick links to each admin task. This guide is for people who manage a CRAFT organisation on behalf of their company. You might be an IT administrator, a Customer Success lead, or a compliance officer. You do not need to be a developer to use the features described here. ## What an org admin can do As an org admin, you manage your organisation's presence on CRAFT. Your responsibilities include: * **Inviting and deactivating users** — control who has access to your organisation * **Assigning roles** — determine what each person can see and do * **Configuring single sign-on (SSO)** — connect CRAFT to your company's identity provider * **Registering data connections** — add the data sources your teams will use * **Reviewing audit logs** — track actions taken by users in your organisation for compliance purposes ## What an org admin cannot do Org admins work within the application layer. You do not have access to the underlying infrastructure: * You cannot access the Kubernetes cluster or server configuration * You cannot modify the CRAFT platform itself — only your organisation's settings * You cannot see data or projects belonging to other organisations * You cannot grant yourself access to a project you have not been given access to — project-level access must be granted by a project owner or admin If you need infrastructure-level changes (for example, adding more storage or adjusting resource limits), contact your platform operator or Emergence support. ## Navigating the admin area All admin tasks are available under **Settings** in the CRAFT platform. After you log in, select **Settings** from the main navigation to access: Invite new users, assign roles, and deactivate accounts. Understand what each role can do and how to assign roles to users. Connect your identity provider so your team can log in with their existing credentials. Add and manage the data sources available to your team. Review and export a record of actions taken in your organisation. ## Who else to contact | Need | Contact | | ----------------------------- | ----------------------------------------------------------------------------- | | Account or billing questions | Your Emergence account manager | | Platform outages or bugs | Emergence support | | Infrastructure changes | Your platform operator | | Onboarding a new project team | Assign a project owner in [Manage Users](/guides/customer-admin/manage-users) | # Register Data Connections Source: https://docs.emergence.ai/guides/customer-admin/register-data-connections Add and manage data sources in CRAFT so your team can ask questions about their data. Data connections let your team point CRAFT at the databases and data sources they want to query and analyse. Once a data connection is registered, it appears as an option when your users start a data conversation in Talk2Data or run a data workflow. Registering data connections requires organisation owner or admin permissions, or project admin permissions for project-scoped connections. ## Where to find data connections In the CRAFT platform, navigate to **Data Connectors** in the main sidebar. This page lists all data connections registered in your organisation. ## Supported connection types CRAFT supports connecting to a range of data source types. The exact list depends on which data connectors have been enabled for your deployment — contact your platform operator if you need a connector that is not shown. Common connection types include: | Type | Examples | | ----------------------- | ------------------------------------------------------------------------------------ | | **Relational database** | PostgreSQL, MySQL, Microsoft SQL Server, Amazon Redshift, Google BigQuery, Snowflake | | **Data warehouse** | Databricks, Azure Synapse | | **File storage** | CSV or Excel files uploaded directly to the platform | ## Register a new data connection Navigate to **Data Connectors** in the main sidebar and select **Add Connection**. Choose the type of data source you want to connect. The form will update to show the fields required for that connection type. Fill in the connection settings for your data source: * **Name**: a descriptive name your team will recognise (for example, "Production Analytics DB" or "Customer Data Warehouse") * **Host / endpoint**: the address of your database server * **Database name**: the specific database to connect to * **Port**: the connection port (CRAFT pre-fills the default for most database types) Use a descriptive name — this is what your users will see when they select a data source to query. "postgres-db-01" is harder to find than "Customer Analytics — Snowflake". Enter the credentials CRAFT should use to connect to your data source. Use a dedicated read-only service account wherever possible — CRAFT only needs read access to run queries. Credentials are stored securely and are not visible after you save the connection. Do not use a personal account or an admin account for the data connection. Use a service account with the minimum permissions required (read-only access to the relevant schemas or tables). Select **Test Connection** before saving. CRAFT will attempt to connect to your data source using the credentials you entered. If the test fails, check: * That the host address and port are correct * That the service account has been granted access to the database from the CRAFT platform's network * That firewall rules or IP allowlists include the CRAFT platform's egress IP addresses (ask your platform operator for these) After a successful connection test, save the connection. You can then assign it to one or more projects so that project members can use it in their data conversations. ## Edit or remove a data connection To update the credentials or settings for an existing connection, select the connection on the **Data Connectors** page and choose **Edit**. To remove a connection, choose **Delete** — this will make the connection unavailable to all projects that use it. Deleting a data connection will break any saved conversations or workflows that reference it. Make sure users are aware before removing a connection that is in active use. ## Assign a connection to a project Data connections can be scoped to specific projects so that only users in that project can access them. To assign a connection, open the connection's detail page and select **Assign to Project**, then choose the project from the list. For more information on how data connections work within the CRAFT platform, see [Data Connections](/platform/data-connections). # Roles and Permissions Source: https://docs.emergence.ai/guides/customer-admin/roles-and-permissions Available roles in CRAFT, what each role can do, and how to assign or remove roles via the UI. CRAFT uses a role-based access system to control what each person can see and do within your organisation and its projects. Roles apply at two levels: the **organisation level** and the **project level**. ## Organisation-level roles Organisation-level roles control access to organisation-wide settings and the ability to manage members. | Role | What they can do | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Owner** | Full access to everything — invite and deactivate members, manage all projects, configure SSO, view audit logs, and manage organisation settings. There must always be at least one owner. | | **Admin** | Invite and deactivate members, create and manage projects, view audit logs. Cannot change organisation billing or ownership. | | **Member** | Access to projects they have been explicitly added to. Cannot manage members or organisation settings. | ## Project-level roles Within a project, users can be assigned more granular roles by a project owner or admin. | Role | What they can do | | ------------- | ----------------------------------------------------------------------------------------- | | **Owner** | Full access to the project — manage members, create and delete resources, manage secrets. | | **Admin** | Manage project members and resources. Cannot delete the project. | | **Developer** | Create, read, and modify resources. Can manage secrets within the project. | | **Operator** | Read resources and trigger workflows. Cannot create or delete resources. | | **Viewer** | Read-only access to resources and metadata within the project. | Organisation owners and admins automatically have admin-level access to all projects in the organisation, even if they are not explicitly added to a project. ## How to assign a role You assign organisation-level roles when inviting a user or by editing their profile on the **Members** page. See [Manage Users](/guides/customer-admin/manage-users) for step-by-step instructions. For project-level roles, a project owner or admin assigns roles from within the project settings. Organisation admins can also assign project roles. ## How to remove a role To remove a user from a project, navigate to the project's **Members** settings and remove them. To change an organisation role, edit the user's profile on the **Members** page. Removing a user from a project does not deactivate their account — they retain access to other projects they belong to and to the organisation. # Data Source Setup Source: https://docs.emergence.ai/guides/data-source-setup How to connect a PostgreSQL database to CRAFT as a data source for querying and governance. # Connect a PostgreSQL Database This guide walks you through registering a PostgreSQL database as a data connection in CRAFT. Once connected, the database is available to solutions like Data Insights (for NL-to-SQL queries) and Data Governance (for profiling and enrichment). ## Prerequisites Before you begin, ensure you have: * A running CRAFT instance * A valid JWT token with the `developer` or `admin` role * Network connectivity between the platform and your PostgreSQL instance * A PostgreSQL user with read access to the target schemas ## How Data Connections Work Data connections are managed by the **Assets** service (reachable at `/api/assets` through the platform gateway). When you register a connection: 1. The connection metadata (host, port, database name) is stored in the Assets database 2. Credentials (username, password) are stored securely via the platform Secrets API (Infisical or ESO + GCP Secret Manager) -- never in the Assets database 3. Solutions retrieve the connection at runtime via the Assets SDK and use the credentials to establish a live database session 4. All connections are scoped to an **organization** and **project** for multi-tenant isolation Data connection credentials are never exposed in API responses. They are injected at runtime only when a solution needs to establish a connection. ## Step 1: Prepare Your Database Ensure your PostgreSQL instance is configured to accept connections from the platform. ```sql theme={null} -- Connect to your PostgreSQL instance as a superuser CREATE USER emergence_reader WITH PASSWORD 'your-secure-password'; -- Grant read access to the target schema GRANT USAGE ON SCHEMA public TO emergence_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO emergence_reader; -- Ensure future tables are also accessible ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO emergence_reader; ``` Update `pg_hba.conf` to allow connections from the platform's network: ```text theme={null} # Allow connections from the Kubernetes cluster CIDR host your_database emergence_reader 192.0.2.0/24 scram-sha-256 ``` Reload the configuration: ```bash theme={null} sudo systemctl reload postgresql ``` From a pod in the platform's Kubernetes cluster, test the connection: ```bash theme={null} psql -h -U emergence_reader -d your_database -c "SELECT 1;" ``` ## Step 2: Register the Data Connection Use the Assets API to register the connection in the platform. ```bash theme={null} curl -X POST "https:///api/assets/data" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "name": "production-analytics", "description": "Production analytics database (read-only)", "connection_type": "postgres", "database_config": { "host": "db.example.com", "port": 5432, "database": "analytics", "ssl_mode": "require" }, "credentials": { "username": "emergence_reader", "password": "your-secure-password" } }' ``` A successful registration returns `201 Created` with the resource: ```json theme={null} { "resource_uri": "data:::production-analytics", "name": "production-analytics", "connection_type": "postgres", "status": "PENDING", "project_id": "", "owner_id": "", "tags": [], "created_at": "2026-05-31T23:40:43.698045Z" } ``` * The platform constructs `resource_uri` as `data:::` (or a kebab-case `resource_uri` override if you supply one in the request). * The `credentials` field is encrypted and stored via the platform Secrets API upon registration. It will **not** appear on subsequent `GET` responses (use `GET /assets/data/{resource_uri}/secret` if a solution needs to read the credentials at runtime). * For object-storage connections (`s3`, `gcs`, `minio`), use `storage_config` instead of `database_config`. For filesystem connections (`nfs`, `smb`), use `filesystem_config`. The complete list of supported `connection_type` values is `GET /api/assets/data/types`. ## Step 3: Test the Connection Verify the platform can reach your database. There are two flavors of verify: ```bash theme={null} # Verify an already-registered connection (idempotent, no body): curl -X POST \ "https:///api/assets/data//verify" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " ``` To verify a candidate connection **before** registration (e.g. as part of a UI wizard), `POST /api/assets/data/verify` with the same body shape as the create request — no `name`/`description` needed: ```bash theme={null} curl -X POST "https:///api/assets/data/verify" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "connection_type": "postgres", "database_config": { "host": "db.example.com", "port": 5432, "database": "analytics" }, "credentials": { "username": "emergence_reader", "password": "..." } }' ``` Both flavors return the same shape: ```json theme={null} { "success": true, "message": "Connection verified", "latency_ms": 15.2, "details": { "server_version": "17.2", "schemas": ["public", "analytics"] } } ``` A failed verify returns `success: false` and a `message` describing the failure mode (`details` and `latency_ms` may be `null`). ## Step 4: Use the Connection with Data Insights Once registered, the connection's `resource_uri` (returned in Step 2) is what you pass to other platform services. The Data Insights solution consumes it from its chat UI / SDK; the underlying HTTP contract lives at `POST /api/talk2data/chat/messages` (paired with `/api/talk2data/chat/sessions` for session management — see the [Data Insights › Text-to-SQL guide](/data-insights/text-to-sql) for the full message-and-session model). The Data Insights UI handles session creation implicitly when a user opens a new chat against this connection. ## Connection Configuration Reference | Parameter | Type | Required | Description | | ---------- | ------- | -------- | --------------------------------------------------------------------------------- | | `host` | string | Yes | Database hostname or IP | | `port` | integer | No | Port number (default: 5432) | | `database` | string | Yes | Database name | | `schema` | string | No | Default schema (default: `public`) | | `ssl_mode` | string | No | SSL mode: `disable` (no TLS) or `require` (TLS, encrypt-only). Default `disable`. | | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ----------------- | | `username` | string | Yes | Database username | | `password` | string | Yes | Database password | ## Security Considerations Credentials are stored via the platform Secrets API (backed by Infisical or ESO + GCP Secret Manager). They are encrypted at rest and organization-scoped. Credentials are injected at runtime only when a solution needs to establish a connection. Configure your database firewall to accept connections only from the platform's Kubernetes cluster CIDR. Use `ssl_mode: require` for production deployments so traffic is TLS-encrypted between the platform and your database. Certificate-pinning modes (`verify-ca`, `verify-full`) are not currently accepted by the Assets API. Always create a dedicated read-only user for the platform. Never use a superuser or an account with write privileges unless the solution explicitly requires write access. ## Troubleshooting Verify network connectivity between the Kubernetes cluster and your database. Check firewall rules, security groups, and `pg_hba.conf` configuration. Ensure the database host is resolvable from within the cluster. Confirm the username and password are correct. Check that the user exists in the target database and has the `CONNECT` privilege. Verify the `pg_hba.conf` entry allows the authentication method being used. If your database requires SSL, set `ssl_mode: require`. Certificate-pinning modes (`verify-ca`, `verify-full`) are not currently accepted by the Assets API — track this if your database requires server-cert verification at the client. ## Next Steps Learn how to ask natural-language questions against your connected database. Profile your connected database to understand data quality and structure. Learn how to back up your platform data including connection configurations. Review network security policies for data connections. # Register Your First Agent Source: https://docs.emergence.ai/guides/first-agent Step-by-step tutorial to register, configure, and test your first A2A agent on CRAFT. # Register Your First Agent CRAFT is an agent platform built on the A2A protocol. Every capability — from natural-language data queries to semiconductor yield analysis — runs as a discrete, independently deployable agent. This guide explains what you can build on CRAFT, who it is for, and where to find the detailed authoring documentation. ## What This Guide Covers This guide walks you through the final step of agent deployment: registering your agent in the CRAFT Assets registry. Once registered, your agent becomes discoverable to orchestrators and other platform consumers via the A2A protocol. Registration involves three steps: authoring an A2A-compliant Agent Card, validating it with the platform's dry-run endpoint, and posting it to the Assets API. You will also learn how to list, fetch, update, and delete registered agents. This tutorial is suitable for developers and operators who have already built and deployed an agent service that exposes an Agent Card at `/.well-known/agent-card.json`. If you have not built your agent yet, start with the framework-specific authoring guides below. ## Who This Guide Is For This guide is written for two audiences: * **Developers** deploying a new agent service to CRAFT for the first time, who need to register it so the orchestrator can discover and invoke it * **Operators** managing the agent registry — listing active agents, promoting lifecycle stages, or cleaning up retired registrations If you are building a new agent from scratch, you need prerequisites beyond what this guide covers: a framework choice, tool authoring, streaming implementation, and evaluation setup. ## Register Your Agent Once your agent service is running and its Agent Card is accessible, use the Assets API to register it on CRAFT. Obtain a JWT token from the platform identity provider to authenticate with the platform APIs. ```bash theme={null} TOKEN=$(curl -s -X POST \ "https:///realms//protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=" \ -d "client_secret=" \ | jq -r '.access_token') ``` For local development, use `make get-token` in the em-runtime repository to obtain a token for the org admin user. Use the validation endpoint to check your Agent Card before registering it. This is a dry-run with no database write. ```bash theme={null} curl -X POST "https:///api/assets/agents:validate" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d @agent-card.json ``` A valid card returns `{"valid": true, "errors": [], "warnings": []}`. ```bash theme={null} curl -i -X POST "https:///api/assets/agents" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "agent_card": '"$(curl -s https://my-agent.example.com/.well-known/agent-card.json)"', "tags": ["demo"], "visibility": "TENANT_ONLY" }' ``` A successful registration returns `201 Created` with a `Location` header and `ETag`. The platform generates a `resource_uri` from the agent name, organization, and project: `agent:{org_id}:{project_id}:{name}`. ```bash theme={null} curl "https:///api/assets/agents?page=1&limit=20" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " ``` Your agent should appear in the listing with `status: ACTIVE` and `lifecycle_stage: EXPERIMENTAL`. ## Lifecycle Stages New registrations default to `EXPERIMENTAL`. The lifecycle stage tracks agent maturity. | Stage | Description | | -------------- | --------------------------------------- | | `EXPERIMENTAL` | Initial registration, under development | | `STABLE` | Production-ready, fully tested | | `DEPRECATED` | Scheduled for removal, still functional | | `RETIRED` | No longer available | Stage transitions are managed by the platform. `UpdateAgentRequest` accepts `agent_card`, `tags`, and `visibility` — not `lifecycle_stage` directly. ## Troubleshooting Verify your token has `can_create_resources` for the target project. Check that the `X-Project-ID` header matches a project you have access to. An agent with the same `name` already exists in the project. Choose a unique name or use `PUT` to update the existing registration. The Agent Card failed validation. Common causes: missing both `url` and `supportedInterfaces`, missing required `name`/`version` fields, or field length limits exceeded (255 chars for `name`, 2000 for `description`). Use `:validate` to surface specific errors. The `ETag` you sent no longer matches — another update happened in between. Fetch the agent again with `GET` to retrieve the latest `ETag`, then retry. ## Next Steps Learn about the full agent registry supporting A2A v0.3 and v1.0 protocols. Connect a database so your agents can query real data. Configure fine-grained permissions for your agents and projects. Explore the full API documentation for the Assets service. # Langfuse Setup Source: https://docs.emergence.ai/guides/langfuse-setup Step-by-step guide to deploying Langfuse and connecting it to all CRAFT solutions for LLM observability. # Langfuse Setup Guide This guide walks through deploying Langfuse and connecting it to your CRAFT solutions. Langfuse provides LLM call tracing, evaluation, and prompt management for Data Insights, Data Governance, and Semiconductor solutions. ## Prerequisites * A running CRAFT deployment (any version) * Docker and Docker Compose (for Docker deployment) or a Kubernetes cluster * Domain name for the Langfuse instance (recommended for production) ## Step 1: Deploy Langfuse ```bash theme={null} # Clone the Langfuse repository (provided in your deployment kit) git clone cd langfuse cp .env .env.local # Edit .env.local with your configuration ``` Edit `.env.local` with your configuration: ```env theme={null} # Required NEXTAUTH_URL=https://langfuse.your-domain.com NEXTAUTH_SECRET= SALT= # Database (auto-provisioned by compose) DATABASE_URL=postgresql://postgres:postgres@langfuse-db:5432/langfuse # Encryption ENCRYPTION_KEY= ``` Start the services: ```bash theme={null} docker compose up -d docker compose ps # verify all services are healthy ``` Langfuse is now available at `http://localhost:3000` (or your configured domain). ```bash theme={null} helm repo add langfuse https://langfuse.github.io/langfuse-k8s helm repo update # Create secrets kubectl create secret generic langfuse-secrets \ --namespace langfuse \ --from-literal=nextauth-secret=$(openssl rand -hex 32) \ --from-literal=salt=$(openssl rand -hex 32) \ --from-literal=encryption-key=$(openssl rand -hex 32) \ --from-literal=pg-password= # Install helm install langfuse langfuse/langfuse \ --namespace langfuse \ --create-namespace \ --set langfuse.nextauth.url="https://langfuse.example.com" \ --set langfuse.nextauth.existingSecret=langfuse-secrets \ --set langfuse.nextauth.existingSecretKey=nextauth-secret \ --set langfuse.salt.existingSecret=langfuse-secrets \ --set langfuse.salt.existingSecretKey=salt ``` ## Step 2: Create a Langfuse Project 1. Navigate to your Langfuse instance 2. Sign up for an account (or log in with the admin account) 3. Create a new **Project** for each solution you want to trace: * `data-insights`, for Data Insights traces * `data-governance`, for Data Governance traces * `semiconductor`, for Semiconductor traces 4. In each project, go to **Settings → API Keys** and copy the **Public Key** and **Secret Key** ## Step 3: Store Credentials Store Langfuse credentials as Kubernetes Secrets (or in your Secrets backend): ```bash theme={null} # Create one secret per solution (use separate Langfuse projects for isolation): kubectl create secret generic langfuse-api-keys \ --namespace em-runtime \ --from-literal=public-key= \ --from-literal=secret-key= kubectl create secret generic langfuse-api-keys-dr \ --namespace em-runtime \ --from-literal=public-key= \ --from-literal=secret-key= kubectl create secret generic langfuse-api-keys-semi \ --namespace em-runtime \ --from-literal=public-key= \ --from-literal=secret-key= ``` ## Step 4: Configure Solutions Set the Langfuse environment variables in each solution's Helm values: ### Data Insights ```yaml theme={null} # Helm values key for em-talk2data (Data Insights solution) em-talk2data: extraEnvVars: - name: LANGFUSE_HOST value: "https://langfuse.example.com" - name: LANGFUSE_PUBLIC_KEY valueFrom: secretKeyRef: name: langfuse-api-keys key: public-key - name: LANGFUSE_SECRET_KEY valueFrom: secretKeyRef: name: langfuse-api-keys key: secret-key ``` ### Data Governance Same configuration, referencing the data-governance project keys: ```yaml theme={null} # Helm values key for em-data-readiness (Data Governance solution) em-data-readiness: extraEnvVars: - name: LANGFUSE_HOST value: "https://langfuse.example.com" - name: LANGFUSE_PUBLIC_KEY valueFrom: secretKeyRef: name: langfuse-api-keys-dr key: public-key - name: LANGFUSE_SECRET_KEY valueFrom: secretKeyRef: name: langfuse-api-keys-dr key: secret-key ``` ### Semiconductor Apply to both the Backend API and AI Agent services: ```yaml theme={null} # Helm values key for em-semi (Semiconductor solution) em-semi: extraEnvVars: - name: LANGFUSE_HOST value: "https://langfuse.example.com" - name: LANGFUSE_PUBLIC_KEY valueFrom: secretKeyRef: name: langfuse-api-keys-semi key: public-key - name: LANGFUSE_SECRET_KEY valueFrom: secretKeyRef: name: langfuse-api-keys-semi key: secret-key ``` ## Step 5: Verify Traces After deploying the configuration, trigger an LLM call in any solution: 1. In Data Insights: ask a natural language question 2. In Data Governance: run a profiling job 3. In Semiconductor: send a chat message to the AI agent Then check the **Traces** tab in Langfuse, you should see the call appear within seconds. ## Troubleshooting 1. Verify `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, and `LANGFUSE_SECRET_KEY` are set correctly in the pod 2. Check that the Langfuse host is reachable from the pod: `kubectl exec -it -- curl -I https://langfuse.example.com` 3. Check solution logs for Langfuse callback errors 4. Verify the public key corresponds to the correct Langfuse project 1. Check pod status: `kubectl get pods -n langfuse` 2. Check worker logs: `kubectl logs -n langfuse -l app=langfuse-worker` 3. Verify the `NEXTAUTH_URL` matches the URL you're accessing # AWS Marketplace Source: https://docs.emergence.ai/guides/marketplace/aws Deploy CRAFT from the AWS Marketplace using private offers, container products, and EDP committed spend drawdown. # AWS Marketplace CRAFT is available on the **AWS Marketplace** as a Container Product. Customers can deploy the platform into their own EKS clusters and apply purchases against their AWS Enterprise Discount Program (EDP) commitments. ## Why AWS Marketplace Customers apply CRAFT purchases against their AWS Enterprise Discount Program (EDP) commitments, removing budget approval friction. Private offers enable AWS field teams to co-sell, earning quota credit and aligning sales incentives. Marketplace-transacted deals are 112% larger on average than direct sales, according to industry research. 3% fee for contract-based pricing, 1.5-3% for private offers. Contract-based pricing is recommended to avoid higher hourly/monthly fees. ## Architecture CRAFT's cloud-agnostic Helm chart deploys the same platform services on EKS as on GKE. The AWS-specific work is scoped to infrastructure modules and marketplace packaging: | Component | Source | | ------------------------- | ------------------------------------------------------------------ | | **Application chart** | `em-runtime` Helm chart (cloud-agnostic) | | **Infrastructure module** | EKS Terraform module (to be built as part of marketplace strategy) | | **Container images** | Published to Amazon ECR via CI/CD multi-registry pipeline | | **Marketplace packaging** | Container product with Helm charts and container images | ## Deployment Model Private offers are managed through the AWS Marketplace Management Portal (AMMP). The sales team configures pricing using contract-based pricing (recommended) with custom dimensions and generates a private offer for the target customer. The customer receives the private offer link, reviews pricing and EULA terms, and subscribes through their AWS Console. Charges apply against their EDP commitment. After subscription, deploy the platform into the customer's EKS cluster using the Terraform module and Helm chart provided with the listing. In the self-service model, the listing includes a CloudFormation template that provisions: * An EKS cluster (or targets an existing one) * Amazon RDS for PostgreSQL (9 databases) * Amazon ElastiCache for Redis * IAM roles and policies for IRSA (IAM Roles for Service Accounts) * Helm chart installation with marketplace-specific values ## Billing Integration AWS billing uses the **MeterUsage** and **RegisterUsage** APIs for consumption tracking. ### Pricing Model AWS Marketplace requires **contract-based pricing** for the most favorable fee structure: | Model | Fee | Mechanism | | -------------------------- | --------------- | ----------------------------------------------------- | | **Contract** | 3% | Customer commits to a term (monthly/annual) | | **Contract + Consumption** | 3% base + usage | Contract base plus metered overage via MeterUsage API | | **Private Offer** | 1.5-3% | Custom pricing per customer | ### Metered Dimensions | Dimension | Unit | API | | ---------------------- | ----- | ------------ | | `agents_registered` | count | `MeterUsage` | | `api_requests` | count | `MeterUsage` | | `data_transfer_out_gb` | GB | `MeterUsage` | | `health_checks` | count | `MeterUsage` | AWS enforces stricter dimension naming conventions than GCP or Azure. Dimension names must use snake\_case and match exactly between the listing definition and the metering API calls. ## Container Image Publishing The CI/CD pipeline publishes images to Amazon ECR as part of the multi-registry strategy: ```yaml theme={null} # ci-main.yml excerpt - name: Push to ECR run: | aws ecr get-login-password --region us-east-1 | \ docker login --username AWS --password-stdin $ECR_REGISTRY docker tag emergence/governance:$VERSION $ECR_REGISTRY/emergence/governance:$VERSION docker push $ECR_REGISTRY/emergence/governance:$VERSION ``` The following images are published: * 3 production services (Governance, Assets, Utils) * 5 subchart images (Keycloak, OpenFGA, PostgreSQL, Redis, Infisical (or ESO)) * 4 init container images (schema migration, bootstrap, health-check, seed data) ## Prerequisites for Deployment Before deploying from AWS Marketplace: 1. **EKS cluster** -- An existing EKS cluster (1.28+) with managed node groups or Fargate profiles 2. **Amazon RDS** -- PostgreSQL 17+ instance with 9 databases 3. **ElastiCache** -- Redis 7 cluster for caching and event streaming 4. **IAM** -- IRSA (IAM Roles for Service Accounts) configured for service-to-AWS authentication 5. **Networking** -- VPC with private subnets and security groups allowing cluster-to-database connectivity ## Comparison with GCP and Azure | Aspect | AWS | GCP | Azure | | ------------------- | ---------------------------- | ----------------------- | ------------------------- | | **Offer Type** | Container Product | Terraform K8s App | Container Offer (K8s App) | | **Packaging** | Container images + Helm | Helm + Terraform module | CNAB bundle | | **Commit Drawdown** | EDP | MCPO | MACC | | **Fee Structure** | 3% contract / 1.5-3% private | 3% / 1.5% renewals | \~3% flat | | **Complexity** | Moderate | Lowest | Highest | ## Troubleshooting Verify the EKS node role has the `ecr:GetDownloadUrlForLayer`, `ecr:BatchGetImage`, and `ecr:GetAuthorizationToken` permissions. Ensure the ECR repository policy allows cross-account access if the customer's EKS cluster is in a different AWS account. Confirm the product code matches the listing ID in AMMP. Ensure the metering calls originate from within the customer's AWS account using IRSA credentials. Verify the customer's EDP agreement covers AWS Marketplace purchases. Some older EDP agreements exclude marketplace spend -- the customer should contact their AWS account team. ## Next Steps Learn about the GCP Marketplace listing and GKE deployment. Learn about the Azure Marketplace listing and AKS deployment. # Azure Marketplace Source: https://docs.emergence.ai/guides/marketplace/azure Deploy CRAFT from the Azure Marketplace using private offers, CNAB bundles, and MACC committed spend drawdown. # Azure Marketplace CRAFT is available on the **Azure Marketplace** as a Container Offer (Kubernetes Application). Customers can deploy the platform into their own AKS clusters and apply purchases against their Microsoft Azure Consumption Commitment (MACC). ## Why Azure Marketplace Customers apply CRAFT purchases against their Microsoft Azure Consumption Commitment (MACC), eliminating budget approval friction for Azure-committed enterprises. Private offers through Partner Center enable Microsoft field teams to co-sell, providing priority access to enterprise accounts. Purchases appear on the customer's existing Azure invoice with no separate vendor billing. Azure Marketplace listings undergo Microsoft certification, providing additional trust signals for enterprise procurement. ## Architecture The Azure listing packages the same cloud-agnostic `em-runtime` Helm chart into a CNAB (Cloud Native Application Bundle) format required by Azure Marketplace: | Component | Source | | ------------------------- | ------------------------------------------------------------------ | | **Application chart** | `em-runtime` Helm chart (cloud-agnostic) | | **Packaging format** | CNAB bundle (Helm + ARM template + createUIDef) | | **Infrastructure module** | AKS Terraform module (to be built as part of marketplace strategy) | | **Container images** | Published to Azure Container Registry (ACR) via CI/CD | | **Partner management** | Azure Partner Center | Azure Marketplace has the highest packaging complexity of the three cloud marketplaces due to the CNAB bundle requirement. The bundle must include an ARM template and a `createUIDef.json` for the deployment UI. ## Deployment Model Private offers are managed through Azure Partner Center. The sales team configures pricing, selects target Azure tenants or subscription IDs, and generates a private offer with custom terms and pricing. The customer receives the private offer notification in the Azure Portal, reviews terms, and subscribes. Charges apply against their MACC commitment. After subscription, the platform is deployed into the customer's AKS cluster using the CNAB bundle, which installs the Helm chart with marketplace-specific values. The self-service deployment uses the CNAB bundle with a `createUIDef.json` that provides: * A guided deployment wizard in the Azure Portal * AKS cluster selection or provisioning * Azure Database for PostgreSQL Flexible Server configuration * Azure Cache for Redis provisioning * Managed Identity configuration for pod-to-Azure-service authentication ## CNAB Bundle Structure The Azure Container Offer requires a CNAB bundle with these components: ```text theme={null} cnab/ bundle.json # CNAB manifest referencing Helm chart and images app/ Chart.yaml # em-runtime Helm chart metadata values.yaml # Marketplace-specific defaults templates/ # Helm templates arm/ mainTemplate.json # ARM template for Azure resource provisioning createUIDef.json # Deployment UI definition for Azure Portal ``` ### createUIDef.json The `createUIDef.json` defines the deployment wizard that customers see in the Azure Portal: * **Basics**: Subscription, resource group, region selection * **AKS Configuration**: Cluster selection or creation parameters * **Database**: Azure Database for PostgreSQL connection settings * **Cache**: Azure Cache for Redis connection settings * **Identity**: Managed Identity configuration for Keycloak and secrets backend (Infisical or ESO) ## Billing Integration Azure billing uses the **Metering Service API** for consumption tracking. ### Pricing Model | Model | Fee | Mechanism | | ----------------- | ---------- | -------------------------------------------- | | **Flat rate** | \~3% | Monthly or annual subscription | | **Custom meters** | \~3% | Per-dimension usage via Metering Service API | | **Private Offer** | Negotiable | Custom pricing per customer | ### Metered Dimensions | Dimension | Unit | Description | | ------------------- | ----- | ------------------------------ | | `agentsRegistered` | count | Active A2A agent registrations | | `apiRequests` | count | Total API requests | | `dataTransferOutGB` | GB | Data egress from the platform | | `healthChecks` | count | Health check probes performed | ## Container Image Publishing The CI/CD pipeline publishes images to Azure Container Registry (ACR): ```bash theme={null} # Authenticate to ACR az acr login --name emergenceai # Tag and push images docker tag emergence/governance:$VERSION emergenceai.azurecr.io/emergence/governance:$VERSION docker push emergenceai.azurecr.io/emergence/governance:$VERSION ``` All 12+ container images (3 production + 5 subchart + 4 init) are published to ACR as part of the multi-registry CI/CD pipeline. ## Prerequisites for Deployment Before deploying from Azure Marketplace: 1. **AKS cluster** -- An existing AKS cluster (1.28+) with system and user node pools 2. **Azure Database for PostgreSQL** -- Flexible Server with PostgreSQL 17+ and 9 databases 3. **Azure Cache for Redis** -- Redis 7 instance for caching and event streaming 4. **Managed Identity** -- User-assigned managed identity for pod-to-service authentication 5. **Networking** -- VNet with AKS integration and private endpoints for database and cache ## Partner Center Certification Azure Marketplace requires additional certification steps: | Requirement | Description | | ------------------------ | ------------------------------------------------------------ | | **Technical validation** | Microsoft validates the CNAB bundle deploys correctly to AKS | | **Security review** | Container images are scanned for vulnerabilities | | **Documentation** | Deployment guide and support documentation required | | **Support plan** | SLA and support contact information must be provided | Partner Center certification can take 2-4 weeks. Plan for this timeline when targeting Azure Marketplace availability. ## Troubleshooting Verify the ARM template in `mainTemplate.json` is valid using `az deployment group validate`. Ensure the `createUIDef.json` schema matches the ARM template parameters. Verify the AKS cluster has the `AcrPull` role assignment on the ACR instance. For cross-subscription deployments, ensure the managed identity has appropriate permissions. Confirm the offer is MACC-eligible in Partner Center. The customer's MACC agreement must include Azure Marketplace purchases -- older agreements may not include this benefit. ## Next Steps Learn about the GCP Marketplace listing and GKE deployment. Learn about the AWS Marketplace listing and EKS deployment. # GCP Marketplace Source: https://docs.emergence.ai/guides/marketplace/gcp Deploy CRAFT from the Google Cloud Marketplace using private offers, Terraform Kubernetes apps, and integrated billing. # GCP Marketplace CRAFT is available on the **Google Cloud Marketplace** as a Terraform Kubernetes Application. Customers can deploy the platform into their own GKE clusters and apply purchases against existing GCP committed spend. ## Why GCP Marketplace Apply CRAFT purchases against existing GCP committed spend via the Marketplace Commerce Platform for Operators (MCPO), with a 25% cap for channel deals. Pre-approved vendor status on GCP Marketplace eliminates lengthy procurement cycles -- up to 4 weeks faster than traditional vendor onboarding. Customers receive one consolidated GCP bill rather than a separate vendor invoice. Standard marketplace fee of 3%, with renewal rates as low as 1.5%. ## Architecture Advantage CRAFT is natively deployed on GKE Autopilot in production. The GCP Marketplace listing leverages the same Helm chart (`em-runtime`) and Terraform modules used for direct deployments, ensuring consistency: | Component | Source | | ------------------------- | ------------------------------------------------------------------- | | **Application chart** | `em-runtime` Helm chart (cloud-agnostic) | | **Infrastructure module** | GKE Terraform module (provided in deployment kit) | | **Container images** | Published to Google Artifact Registry via CI/CD | | **Configuration** | Environment-specific Helm values files (provided in deployment kit) | ## Deployment Model Private offers allow the sales team to send a pre-configured pricing link directly to target customers. No public storefront is required. Using the GCP Producer Portal, the sales team configures pricing (subscription, usage-based, or hybrid) and generates a private offer link. The customer clicks the private offer link, reviews pricing, and accepts within their GCP console. The purchase is applied against their GCP committed spend. After acceptance, the customer or the Emergence team deploys the platform into the customer's GKE cluster using the provided Terraform module. In the self-service model, customers click "Deploy" in the GCP Marketplace listing and the platform deploys automatically into their GKE cluster. The deployment uses: * A `schema.yaml` defining customer-configurable parameters * Terraform modules for GKE cluster, Cloud SQL, Memorystore, and GCS provisioning * Helm chart installation with marketplace-specific values * Workload Identity for secure service-to-service authentication ## Billing Integration GCP billing uses a metering agent sidecar that reports usage metrics to the GCP Marketplace Billing API. ### Metered Dimensions Metering dimensions are not yet finalized. The dimensions below are candidates under review by the product team. Final dimensions will be aligned with customer value metrics before the billing integration goes live. | Candidate Dimension | Unit | Description | Status | | --------------------- | ----- | -------------------------------------------------- | ------------ | | Active users | count | Active platform users per billing period | Under review | | API calls | count | Total API requests (aggregate or by service) | Under review | | Data volume processed | bytes | Total data volume processed by the platform | Under review | | Managed nodes | count | Number of managed infrastructure nodes or clusters | Under review | ### Pricing Model The GCP listing supports a hybrid pricing model: * **Base subscription**: Monthly or annual fee for platform access * **Usage-based metering**: Additional charges based on consumption of metered dimensions * **Private offer pricing**: Custom pricing negotiated per customer via the Producer Portal ## Container Image Publishing The CI/CD pipeline publishes container images to Google Artifact Registry for the following components: | Category | Images | | ------------------------- | -------------------------------------------------------- | | **Production services** | Governance, Assets, Utils | | **Subchart dependencies** | Keycloak, OpenFGA, PostgreSQL, Redis, Infisical (or ESO) | | **Init containers** | Schema migration, bootstrap, health-check | The `ci-main.yml` pipeline builds and pushes images to Artifact Registry on every merge to main. Pre-release images are built from feature branches via `ci-branches.yml`. ## Prerequisites for Deployment Before deploying from GCP Marketplace: 1. **GKE cluster** -- An existing GKE Autopilot or Standard cluster (1.28+) 2. **Cloud SQL** -- PostgreSQL 17+ instance with 9 databases (one per service) 3. **Memorystore** -- Redis 7 instance for caching and event streaming 4. **IAM** -- Workload Identity configured for service-to-GCP-service authentication 5. **Networking** -- VPC with private service access for Cloud SQL and Memorystore ## Troubleshooting Verify the deploying user has the `roles/container.admin` and `roles/iam.serviceAccountUser` roles. Workload Identity requires the service account to be bound to the Kubernetes service account. Check that the metering agent sidecar is running in the Governance pod. Verify the `MARKETPLACE_METERING_ENABLED` environment variable is set to `true` and the service account has the `roles/servicemanagement.reporter` role. Ensure the private offer was created in the correct GCP Producer Portal project and the customer's billing account ID matches the offer target. ## Next Steps Learn about the AWS Marketplace listing and EKS deployment. Learn about the Azure Marketplace listing and AKS deployment. Configure Helm values for GKE deployment. # Memory Integration Source: https://docs.emergence.ai/guides/memory-integration How-to guide for integrating the Memory Service into your agents, creating and managing memories via the Platform Utils API. # Memory Integration This guide shows how to integrate memory into your agents and workflows. Memory lets agents remember information across sessions, accumulate domain knowledge, and personalize responses. Memory is built into the **Utils service** (`em-runtime-utils`) — the same service that handles schedules, metadata, and catalog tags. No separate service is required. ## Prerequisites * A valid JWT token from the platform (user or service account) * The JWT must contain an `org_id` claim for organization scoping * An `X-Project-ID` header for project scoping on all memory requests ## Basic Integration Pattern The most common pattern for an agent: 1. **Before responding**, list memories from the relevant Context Pack for context 2. **After responding**, extract and store new memories from the conversation ```python theme={null} import httpx UTILS_BASE = "https://api.example.com/utils" async def agent_respond(user_message: str, token: str, pack_name: str, project_id: str) -> str: headers = { "Authorization": f"Bearer {token}", "X-Project-ID": project_id, } async with httpx.AsyncClient() as client: # Step 1: List memories from the relevant Context Pack mem_resp = await client.get( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, ) memories = mem_resp.json().get("data", []) # Step 2: Build context from memories context = "\n".join(f"- {m['content']}" for m in memories) # Step 3: Call LLM with memory context prompt = f"Relevant context:\n{context}\n\nUser: {user_message}" response = await call_llm(prompt) # Step 4: Store new memories from this interaction if should_memorize(user_message, response): await client.post( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, json={ "name": "session_fact", "content": { "type": "text", "text": extract_memorable_fact(user_message, response), }, "memory_type": "fact", "source": {"type": "agent"}, }, ) return response ``` ## Storing Memories The following snippets show individual API calls. In an async agent, wrap them in `async with httpx.AsyncClient() as client:` and use `await client.post(...)` / `await client.get(...)` as shown in the integration pattern above. ### Basic Memory Creation Memories are created within a named Context Pack: ```python theme={null} # Store a user preference in a context pack await client.post( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, json={ "name": "metric_explanation_style", "content": { "type": "text", "text": "User prefers metric definitions before SQL queries", }, "memory_type": "preference", "source": {"type": "user"}, }, ) ``` ### Memory Types Choose the type based on the nature of the information: | Type | When to use | Example | | ----------------- | ----------------------- | -------------------------------------------------------- | | `fact` | Factual knowledge | "The `orders` table has 50M rows" | | `experience` | Learned behaviors | "Query optimization worked better with CTEs" | | `observation` | Observed patterns | "Revenue spikes every Q4" | | `instruction` | Directives / guidelines | "Always include date filters for large tables" | | `preference` | User/system preferences | "Prefers concise output without verbose explanations" | | `summary` | Condensed summaries | "Session covered revenue analysis across 3 regions" | | `glossary` | Business terms | "ARR = Annual Recurring Revenue" | | `join` | Table relationships | "Table `orders` has FK to `customers`" | | `ontology` | Domain classifications | "Column `status` uses enum: active, inactive, churned" | | `policy` | Governance rules | "PII columns must not appear in SQL results" | | `textual_pattern` | Text patterns in data | "Email column matches a standard email regex pattern" | | `kpi` | KPI definitions | "Churn Rate = lost customers / total customers x 100" | | `exemplar` | Representative examples | "Typical order: id=1234, amount=49.99, status=completed" | | `numeric_pattern` | Numeric distributions | "Revenue column: mean=1250, median=980, stddev=450" | ### Named Memories Use `name` for memories you want to retrieve directly by name within a pack: ```python theme={null} # Store with a name for direct retrieval await client.post( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, json={ "name": "user_data_source_primary", "content": { "type": "text", "text": "User's primary data source is Snowflake, database: analytics_prod", }, "memory_type": "fact", "source": {"type": "user"}, }, ) # Retrieve by name within a pack response = await client.get( f"{UTILS_BASE}/context-packs/{pack_name}/memories/user_data_source_primary", headers=headers, ) # Retrieve by memory ID response = await client.get( f"{UTILS_BASE}/memories/{memory_id}", headers=headers, ) ``` ## Retrieving Memories ### List All Memories in a Context Pack ```python theme={null} response = await client.get( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, ) memories = response.json().get("data", []) for memory in memories: print(f"[{memory['memory_type']}] {memory['content']}") ``` ### List All Memories in the Project ```python theme={null} response = await client.get( f"{UTILS_BASE}/memories", headers=headers, ) all_memories = response.json() # {"data": [...], "pagination": {...}} ``` ## Context Packs For structured memory management, use Context Packs to group related memories by domain: ```python theme={null} # Create a domain knowledge pack pack_resp = await client.post( f"{UTILS_BASE}/context-packs", headers=headers, json={ "name": "user_data_domain", "pack_type": "domain", "description": "User's data domain knowledge: schema, metrics, business rules", }, ) pack_name = pack_resp.json()["name"] # Add a memory to the pack await client.post( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, json={ "name": "revenue_metric_definition", "content": { "type": "text", "text": "Revenue metric = sum of completed order amounts, excluding refunds", }, "memory_type": "fact", "source": {"type": "agent"}, }, ) # Retrieve all memories from the pack response = await client.get( f"{UTILS_BASE}/context-packs/{pack_name}/memories", headers=headers, ) memories = response.json().get("data", []) ``` ## Updating and Deleting ### Update a Memory ```python theme={null} # When a fact changes, update rather than delete await client.patch( f"{UTILS_BASE}/memories/{memory_id}", headers=headers, json={ "content": { "type": "text", "text": "User's primary data source is BigQuery, project: analytics_prod (migrated from Snowflake)", }, }, ) ``` ### Delete a Memory ```python theme={null} # Soft delete (recoverable) await client.delete( f"{UTILS_BASE}/memories/{memory_id}", headers=headers, ) # Permanent delete (irreversible) await client.post( f"{UTILS_BASE}/memories/{memory_id}/permanent-delete", headers=headers, ) ``` ### Pin a Memory Pinned memories stay in the Active tier and bypass inactivity decay: ```python theme={null} await client.post( f"{UTILS_BASE}/memories/{memory_id}/pin", headers=headers, ) ``` ## Best Practices Each memory should make sense without knowing the conversation that produced it. Instead of "Yes, that's right" store "User confirmed that the 'revenue' column uses gross revenue, not net." Typed memories retrieve more accurately. If you know a memory is a preference, mark it `preference`, use specific types for better retrieval. Before creating a new memory, list the pack's memories and check whether an existing one covers the same information. Update existing memories rather than creating duplicates. For domain-specific knowledge bases (data dictionaries, business rules, entity relationships), group memories into typed Context Packs. This improves retrieval precision and enables pack-level operations. Memories are scoped by the `org_id` claim in the JWT token and the `X-Project-ID` header. Ensure each agent uses a token scoped to the correct organization and passes the correct project ID to prevent information leakage. ## Related Concepts overview and API reference. Detailed Context Pack architecture and management. # RBAC Configuration Source: https://docs.emergence.ai/guides/rbac-configuration How to set up OpenFGA roles and permissions for fine-grained access control across organizations, projects, and resources. # Configure Roles and Permissions CRAFT uses **OpenFGA** for Relationship-Based Access Control (ReBAC). This guide covers setting up roles, assigning permissions, and configuring the permission inheritance model across organizations, projects, and resources. ## Prerequisites Before you begin, ensure you have: * A running CRAFT with the Governance service bootstrapped * Administrative access (the `admin` or `owner` role on the target organization) * A valid JWT token for the admin user * The OpenFGA schema loaded during Governance bootstrap ## How Permissions Work The platform uses a **relationship-based** model rather than traditional role-based access control. Permissions are computed at query time from relationships between users, roles, and resources. ```text theme={null} User --[role]--> Organization --[parent]--> Project --[parent]--> Resource ``` ### Role Hierarchy | Role | Scope | Capabilities | | ----------- | ---------------------- | ------------------------------------------------------------------------ | | `owner` | Organization | Full control, can transfer ownership, manage all settings | | `admin` | Organization / Project | Manage users, projects, settings. Cannot transfer organization ownership | | `member` | Project | Standard access to project resources | | `developer` | Project | Create and modify agents, data connections, workflows | | `operator` | Project | Deploy, schedule, and monitor resources | | `viewer` | Project | Read-only access to all resources | ### Computed Permissions Permissions are derived from roles via the OpenFGA schema: | Permission | owner | admin | member | developer | operator | viewer | | -------------------- | ----- | ----- | ------ | --------- | -------- | ------ | | `can_read` | Yes | Yes | Yes | Yes | Yes | Yes | | `can_write` | Yes | Yes | Yes | Yes | No | No | | `can_delete` | Yes | Yes | No | No | No | No | | `can_execute` | Yes | Yes | Yes | Yes | Yes | No | | `can_manage_secrets` | Yes | Yes | No | No | No | No | ## Step 1: Understand the OpenFGA Schema The permission model is defined in the [OpenFGA Schema DSL](https://openfga.dev/docs/configuration-language). Key type definitions used by the platform: ```text theme={null} type organization relations define owner: [user] define admin: [user] or owner define member: [user] or admin type project relations define parent: [organization] define admin: [user] or admin from parent define developer: [user] or admin define operator: [user] or admin define viewer: [user] or developer or operator define can_read: viewer or member from parent define can_write: developer or admin define can_delete: admin define can_execute: operator or developer or admin ``` ## Step 2: Grant a role on a resource The Governance API exposes a single grant endpoint that is used for every role-on-resource combination — there is no separate "add organization member" or "add project member" endpoint. A "role assignment" is a tuple `(user_or_group, relation, resource_type:resource_id)` written to the OpenFGA store via `POST /governance/permissions/grant`. ```bash theme={null} # Grant the 'admin' relation to a user on an organization curl -X POST "https:///api/governance/permissions/grant" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "user_or_group": "", "relation": "admin", "resource_type": "organization", "resource_id": "" }' ``` Response (`200 OK`): ```json theme={null} { "message": "Granted admin permission to user '' on organization ''" } ``` The same endpoint also assigns project-scoped roles — change `resource_type` to `project` and `resource_id` to the project UUID: ```bash theme={null} curl -X POST "https:///api/governance/permissions/grant" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "user_or_group": "", "relation": "developer", "resource_type": "project", "resource_id": "" }' ``` Same shape for any resource type: `artifact`, `data_connection`, `secret`, etc. The `relation` must be one of the role names defined on that resource type in the OpenFGA schema (Step 1). The auto-generated client is an [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) package — module-level functions per endpoint, called against an `AuthenticatedClient` instance. There is no nested `client.permissions.grant(...)` method. ```python theme={null} from em_runtime_governance_sdk import AuthenticatedClient from em_runtime_governance_sdk.api.permissions import grant_permission from em_runtime_governance_sdk.models import GrantPermissionRequest client = AuthenticatedClient( base_url="https:///api/governance", token=admin_token, ) # Grant on an organization await grant_permission.asyncio( client=client, body=GrantPermissionRequest( user_or_group=user_id, relation="admin", resource_type="organization", resource_id=org_id, ), ) # Grant on a project — same call, different resource_type/resource_id await grant_permission.asyncio( client=client, body=GrantPermissionRequest( user_or_group=user_id, relation="developer", resource_type="project", resource_id=project_id, ), ) ``` The same pattern applies to the other permission endpoints — import `revoke_permission`, `check_permission`, `list_accessible_objects`, `set_resource_parent`, or `delete_resource_permissions` from the same `em_runtime_governance_sdk.api.permissions` module. ### Permission Inheritance Roles inherit downward through the hierarchy because the OpenFGA schema expresses each role on a child as derivable from its parent: Automatically has admin-level permissions on all projects and resources within the organization. Automatically has admin-level permissions on all projects. Can create and delete projects. Has `can_read`, `can_write`, and `can_execute` on all resources within the specific project. Cannot delete resources or manage secrets. Has `can_read` only. Cannot modify, execute, or delete any resource. When you create a child resource (e.g. an artifact within a project), register the parent relationship so inheritance applies: ```bash theme={null} curl -X POST "https:///api/governance/permissions/set-parent" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "resource_type": "artifact", "resource_id": "", "parent_type": "project", "parent_id": "" }' ``` ## Step 3: Check or list permissions Two read endpoints answer the common questions: ```bash theme={null} # 1) Can perform on ? # Note: this is a GET with query params — NOT a POST with a body. curl -G "https:///api/governance/permissions/check" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ --data-urlencode "action=can_write" \ --data-urlencode "resource_type=agent" \ --data-urlencode "resource_id=" ``` Response (`200 OK`): the response body for an allowed call is `null` (absence of a `403` body means the action is permitted). A denied check returns `403 Forbidden`. ```bash theme={null} # 2) Which objects can read across every resource type? curl "https:///api/governance/permissions/accessible-objects" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " ``` Response (`200 OK`): ```json theme={null} { "object_ids": [ "project:", "data_connection:data:::", "agent:agent:::" ] } ``` Each entry is `:`, with `resource_id` echoing the canonical `resource_uri` for `agent` / `data_connection` / `artifact` resources. ## Step 4: Revoke a role `POST /governance/permissions/revoke` mirrors `grant`: ```bash theme={null} curl -X POST "https:///api/governance/permissions/revoke" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "user_or_group": "", "relation": "developer", "resource_type": "project", "resource_id": "" }' ``` Response (`200 OK`): ```json theme={null} { "message": "Revoked developer permission from user '' on project ''" } ``` To delete every permission tuple for a resource (e.g. before deleting the resource itself), use `POST /governance/permissions/delete-all`: ```bash theme={null} curl -X POST "https:///api/governance/permissions/delete-all" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: " \ -H "Content-Type: application/json" \ -d '{ "resource_type": "project", "resource_id": "" }' ``` Response includes `deleted_count` so you can confirm the sweep. ## Common Permission Patterns Assign `viewer` on the project containing data connections and dashboards. Users can run queries via Data Insights but cannot modify data connections or agent configurations. Assign `operator` on the target project. Operators can deploy agents, trigger schedules, and monitor health, but cannot modify agent code or data connection credentials. Create Keycloak client credentials and assign `developer` or `operator` on the target project. Service accounts authenticate via client credentials grant and have the same permission model as human users. A user can have different roles on different projects within the same organization. Assign roles per project to implement least-privilege access. ## Integration with SSO Groups When SSO is configured, IdP groups map through Keycloak to OpenFGA: ```text theme={null} IdP Group -> Keycloak Group -> OpenFGA Relation (role assignment) ``` This pipeline automates role assignment when users are provisioned via SSO. See the [SSO Integration guide](/guides/sso-integration) for configuring the IdP side. ## Troubleshooting Verify the OpenFGA schema is loaded by checking Governance bootstrap logs. Ensure the user's JWT contains the correct groups claim. Confirm the user has been assigned a role on the target project. Check that the project has a `parent` relationship to the organization in OpenFGA. This is set automatically when projects are created via the Governance API. OpenFGA evaluates permissions at query time -- there is no caching delay. If changes are not reflected, verify the role assignment was successful by listing members on the project. ## Next Steps Configure SSO to automate group-to-role mapping from your IdP. Learn about the full OpenFGA schema and computed permissions model. Understand how permissions integrate with the multi-tenant architecture. Review the complete security model including authentication and data protection. # Access LLMs Source: https://docs.emergence.ai/guides/solution-dev/access-llms Call LLMs from your solution through the platform's LiteLLM gateway — provider-agnostic, observable via Langfuse, with project-scoped cost attribution. # Access LLMs Solutions never call OpenAI, Anthropic, or Gemini directly. Instead, point an OpenAI-compatible SDK at the platform's **LiteLLM gateway**. You get provider-agnostic access, central API-key management, per-project cost attribution, rate-limit enforcement, and Langfuse traces — for free. The canonical reference implementation is in [Data Insights › Text-to-SQL](/data-insights/text-to-sql); this page extracts the patterns every solution needs. ## Why a gateway | Without a gateway | With LiteLLM gateway | | ------------------------------------------------------- | ------------------------------------ | | Each solution holds its own API keys | One key per provider, held centrally | | Switching from OpenAI → Anthropic = code change | Switching is an env var change | | Per-solution cost attribution requires custom telemetry | Free via gateway metadata + Langfuse | | Rate limits enforced per-key (poorly) | Enforced centrally, per project | | Adding observability requires per-call wrapping | Auto-instrumented via Langfuse hook | ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR SOL["Your solution
litellm.acompletion(...)"] META["metadata=
{project_id, solution}"] SOL -.includes.-> META GW["LiteLLM gateway
(provider-agnostic)"] SOL -->|"OpenAI-compatible
HTTPS"| GW GW --> OPENAI[("OpenAI")] GW --> ANTH[("Anthropic")] GW --> GEMINI[("Google Gemini")] GW --> OTHER[("...other
providers")] LANGFUSE[("Langfuse
(traces + cost)")] GW -.auto-traces.-> LANGFUSE META -.attributes spend by.-> LANGFUSE classDef sol fill:#56B4E9,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef ext fill:#CC79A7,stroke:#555555,color:#000 classDef act fill:#E69F00,stroke:#555555,color:#000 classDef store fill:#009E73,stroke:#555555,color:#000 class SOL sol class GW infra class OPENAI,ANTH,GEMINI,OTHER ext class META act class LANGFUSE store ``` **Read the diagram**: your solution calls one OpenAI-compatible endpoint; the gateway routes by `provider/model` to the chosen upstream and auto-traces every call into Langfuse. The `metadata={"project_id": ..., "solution": ...}` you pass on every call is what makes per-project cost attribution possible. ## Configure your service Three env vars (plus Langfuse credentials when you want tracing): ```yaml charts//values.yaml theme={null} api: env: LLM_GATEWAY_URL: "https://litellm.example.com/v1" # platform operator provides this LLM_DEFAULT_MODEL: "openai/gemini-3.5-flash" envVars: - name: LLM_GATEWAY_API_KEY valueFrom: { secretKeyRef: { name: -secrets, key: llm-gateway-api-key } } # Observability (Langfuse) — see /guides/langfuse-setup - name: LANGFUSE_HOST valueFrom: { configMapKeyRef: { name: -langfuse, key: host } } - name: LANGFUSE_PUBLIC_KEY valueFrom: { secretKeyRef: { name: -secrets, key: langfuse-public-key } } - name: LANGFUSE_SECRET_KEY valueFrom: { secretKeyRef: { name: -secrets, key: langfuse-secret-key } } ``` The gateway URL and API key live in your secrets pipeline (see [Manage Secrets](/guides/solution-dev/manage-secrets)). ## Code patterns ### Minimal call (litellm) `litellm` speaks the OpenAI completions wire format and works against any compatible endpoint. Install with `uv add litellm`. ```python packages/api/src/api/llm.py theme={null} import os import litellm litellm.api_base = os.environ["LLM_GATEWAY_URL"] litellm.api_key = os.environ["LLM_GATEWAY_API_KEY"] DEFAULT_MODEL = os.environ.get("LLM_DEFAULT_MODEL", "openai/gemini-3.5-flash") async def complete(prompt: str, *, model: str | None = None, project_id: str, solution: str) -> str: response = await litellm.acompletion( model=model or DEFAULT_MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=1024, # set generously — Gemini 3.5-flash uses thinking tokens before text # metadata flows to Langfuse + the gateway's cost-attribution layer metadata={ "project_id": project_id, "solution": solution, "trace_id": "auto", }, ) return response.choices[0].message.content ``` **Use the `openai/` prefix** when pointing litellm at a custom proxy (the CRAFT gateway). Without it, litellm infers the provider from the model name and routes directly to Anthropic/Vertex instead of through the proxy. `openai/gemini-3.5-flash` tells litellm "use the OpenAI wire format, route to whatever `api_base` says" — the gateway then forwards to the correct upstream. **Set `max_tokens ≥ 200`** for Gemini 3.5-flash at the `global` Vertex endpoint. The model uses adaptive thinking by default, consuming thinking tokens before text tokens. With `max_tokens=30`, all tokens go to thinking and the response is empty. The token budget needs room for both. ### Per-call model override The default model is configurable at deploy time, but any call site can override it: ```python theme={null} # Default for most calls — fastest + highest quality on agentic / coding benchmarks fast_answer = await complete("…", model="openai/gemini-3.5-flash", project_id=p, solution=s) # Only when extensive custom tool calling is core to this code path # (e.g. the model otherwise calls `cat`/`grep`/`sed` instead of your registered tools) tool_heavy = await complete("…", model="openai/gemini-3.1-pro-preview-customtools", project_id=p, solution=s) # Secondary provider — demonstrates model agnosticity secondary = await complete("…", model="openai/claude-opus-4-8", project_id=p, solution=s) # Local dev with no external dependency local_dev = await complete("…", model="ollama/llama3.1", project_id=p, solution=s) ``` The gateway routes by the model name you pass. Use the `openai/` prefix for models served over the OpenAI-compatible wire format (the common case for all gateway-hosted models); provider-native prefixes such as `ollama/` apply only for local sidecars not routed through the gateway. Ask the platform operator for the current model list. **Use `gemini-3.1-pro-preview-customtools` only when extensive custom tool calling is a core requirement of your agent.** It's a fine-tuned variant of `gemini-3.1-pro` that prioritizes your registered custom functions over bash fallbacks — same intelligence as base 3.1 Pro, narrowed for one job. **Google's own guidance**: if >50% of requests don't involve tool calling, stay on `gemini-3.5-flash`; quality on non-tool workloads is measurably lower on the customtools variant ([Apiyi: When to Switch to Customtools](https://help.apiyi.com/en/gemini-3-1-pro-preview-customtools-agent-guide-en.html)). `gemini-3.5-flash` already beats `gemini-3.1-pro` on agentic benchmarks (MCP Atlas +5.4%) and coding (Terminal-Bench +6%) at 3.6× the speed ([Gemini 3.5 launch data](https://wavespeed.ai/blog/posts/gemini-3-5-pro-flash/)) — so reach for the customtools variant only when you've **measured** that bash fallbacks are overriding your registered tools (Apiyi suggests: when bash usage exceeds 30% of actions that could have been handled by registered tools). Operational notes when you do use it: preview SLA (may be deprecated or renamed without the GA notice period), lower quota than GA models (will 429 sooner under sustained load — wrap in a fallback chain via [Failure modes](#failure-modes) below), global-endpoint only (`vertex_location` must be `global`), and **Provisioned Throughput (PT) is not supported**. ### Streaming (SSE) ```python theme={null} async def stream_completion(prompt: str, *, project_id: str, solution: str): response = await litellm.acompletion( model=DEFAULT_MODEL, messages=[{"role": "user", "content": prompt}], stream=True, metadata={"project_id": project_id, "solution": solution}, ) async for chunk in response: delta = chunk.choices[0].delta.content if delta: yield delta ``` Wrap in a FastAPI `StreamingResponse` to expose to your UI as Server-Sent Events. ## Observability — Langfuse When `LANGFUSE_HOST`, `LANGFUSE_PUBLIC_KEY`, and `LANGFUSE_SECRET_KEY` are set, `litellm` auto-emits traces. Each call shows up as a trace with the `metadata` you passed (project, solution, latency, token counts, model, prompt+completion). See [Guides › Langfuse Setup](/guides/langfuse-setup) and [Deployment › Observability › Langfuse](/deployment/observability/langfuse). Add `litellm.success_callback = ["langfuse"]` once at startup (the gateway's reference implementation does this in `commons/llm/__init__.py` of `em-talk2data`). For LLM-specific observability patterns, see [Deployment › Observability › LLM Observability](/deployment/observability/llm-observability). ## Cost attribution The `metadata={"project_id": ..., "solution": ...}` you pass on each call is the *only* thing that gives the platform per-project cost attribution. **Always pass it.** Without it, the call rolls up to a generic bucket and the cost dashboard cannot tell you which project caused the spike. If you wrap `litellm.acompletion` in a service helper (recommended), make `project_id` a required parameter so it's impossible to call without it. ## Failure modes The gateway enforces per-project rate limits. Back off exponentially (`tenacity` with `wait_random_exponential`) and surface a friendly message to the user. Do not retry forever — let the user see the rate limit. Either the gateway is down (rare) or the upstream provider rejected the request. Retry once with backoff, then fall back to a different model — cross-provider first for resilience (`fallbacks=["openai/claude-opus-4-8", "openai/gemini-3.1-pro-preview"]` is a `litellm` parameter — both route through the gateway's `model_list` to Vertex AI). **Don't list `gemini-3.1-pro-preview-customtools` as a general fallback** — it's specialized for tool-heavy agents and may degrade quality for non-tool workloads. `LLM_GATEWAY_API_KEY` rotated and your pod hasn't restarted. If you've configured Stakater Reloader on the secret (default for `em-service`), it should already be rolling. If not, `kubectl rollout restart` the deployment manually. The model you requested isn't routed by the gateway. Check the gateway's model list with `curl $LLM_GATEWAY_URL/models -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" | jq '.data[].id'`. A spike with no project attribution means a code path is calling without `metadata`. Grep for `litellm.acompletion(` and confirm every call passes `metadata={"project_id": ..., "solution": ...}`. See [Troubleshooting › LLM](/guides/solution-dev/troubleshooting#llm) for more. ## Verification ```bash theme={null} # Confirm the gateway is reachable from the pod kubectl -n em- exec deployment/-api -- \ sh -c 'curl -s -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" $LLM_GATEWAY_URL/models | head -c 200; echo' # Confirm a call attributes correctly in Langfuse curl -s -H "Authorization: Bearer $TOKEN" -H "X-Project-ID: $PROJECT_ID" \ http://localhost:8000/llm/test # your route that issues a complete() call # Then look in Langfuse for the trace tagged project_id=$PROJECT_ID ``` For gateway internals from an operator perspective (model allowlist, rate limits, provider routing), see [LLM Gateway](/platform/llm-gateway). ## Next steps Stand up Langfuse and wire it to your service. The platform-side observability stack. Trace inspection, cost dashboards, model comparison. Reference implementation that uses this pattern in production. # Architecture for Solution Developers Source: https://docs.emergence.ai/guides/solution-dev/architecture-for-solution-developers The integration surface a solution sees — what the platform provides, what your solution owns, and the contracts between them. # Architecture for Solution Developers This page explains the platform from the solution developer's perspective: what you can rely on, what you own, and what contracts cross the boundary. It is *not* a complete platform architecture deep-dive — for that, see [Architecture Overview](/getting-started/architecture). ## Three layers, your job is the top one CRAFT has three architectural layers. Solutions live at the top: | Layer | What lives here | Who owns it | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | **Solutions** | Domain apps (Data Insights, Data Governance, your new solution) | Solution teams | | **Platform** | Identity (Keycloak), authorization (OpenFGA), Governance/Assets/Utils services, secrets, storage abstraction, LLM gateway, microfrontend shell | Platform team | | **Infrastructure** | Cloud provisioning, GitOps, networking, cluster lifecycle | SRE / Infra team | Your solution **inherits** platform capabilities — never reimplements them. You do not run your own Keycloak. You do not write your own OpenFGA schema. You delegate via SDK calls and let the platform do the work. ## What the platform provides ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB subgraph Solution["Your solution (em-<solution> namespace)"] UI["UI
(microfrontend)"] API["API service
(FastAPI)"] WORKER["Worker / agent
(optional)"] DB[("Your DB
(Postgres)")] UI --> API API --> WORKER API --> DB end subgraph Platform["Platform (em-runtime namespace)"] KC["Keycloak
(OIDC/JWT)"] GOV["Governance
(8000)"] ASSETS["Assets
(8000)"] UTILS["Utils
(8000)"] SECRETS["Secrets
(Infisical/ESO)"] OBSTORE["Object storage
(obstore)"] LITELLM["LiteLLM gateway"] end subgraph External["External"] MODELS["Model providers
(OpenAI, Anthropic, Gemini, ...)"] end API -->|JWT| KC API -->|"register agents,
connections"| ASSETS API -->|permission checks| GOV API -->|secrets at runtime| SECRETS API -->|read/write blobs| OBSTORE API -->|inference| LITELLM LITELLM --> MODELS classDef solution fill:#56B4E9,stroke:#555555,color:#000 classDef platform fill:#0072B2,stroke:#555555,color:#fff classDef store fill:#009E73,stroke:#555555,color:#000 classDef external fill:#CC79A7,stroke:#555555,color:#000 class UI,API,WORKER solution class KC,GOV,ASSETS,UTILS,SECRETS,OBSTORE,LITELLM platform class DB store class MODELS external ``` | Capability | Where | What you do | | ----------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Authentication | Keycloak (multi-realm OIDC/PKCE) | Validate JWTs in your service. See [Authenticate Users](/guides/solution-dev/authenticate-users). | | Authorization | OpenFGA via Governance SDK | Forward the user JWT + resource ID; receive allow/deny. | | Multi-tenancy | `org_id` (from JWT) + `X-Project-ID` header | Read both from request context; scope all queries by them. See [Concepts](/getting-started/concepts). | | Resource registry | Assets API (port 8000) | Register agents, data connections, files, models. See [Register a Solution](/guides/solution-dev/register-a-solution). | | Secrets | Infisical (self-hosted) or ESO + GCP Secret Manager (cloud) | Declare in your chart's `envVars`; read from `os.environ` or `/mnt/secrets/`. See [Manage Secrets](/guides/solution-dev/manage-secrets). | | Shared storage | `obstore` abstraction over S3, GCS, Azure Blob, MinIO | Initialize a client from a Connection; `put`/`get`/`list`. See [Use Shared Storage](/guides/solution-dev/use-shared-storage). | | LLM access | LiteLLM gateway (provider-agnostic) | Set `LLM_GATEWAY_URL`; call with the OpenAI SDK. See [Access LLMs](/guides/solution-dev/access-llms). | | Observability | Langfuse for LLM traces; OpenTelemetry for everything else | Set `LANGFUSE_HOST`; instrument with OTel SDK. See [Langfuse Setup](/guides/langfuse-setup). | | UI shell | `em-runtime-ui` (React Module Federation 2.0) | Expose your UI as a remote module. (Out of scope for v1 of this guide.) | ## What your solution owns You own everything that is specific to your domain: * **The UI** (typically a React microfrontend remote module loaded into `em-runtime-ui`) * **The backend services** (FastAPI + uvicorn is the platform default; nothing stops you choosing differently) * **Your own PostgreSQL database** — *no cross-service foreign keys with platform DBs*. Each service owns its data; consistency across services is by API contract, not joins. * **Your Helm chart** that wraps `em-service` (one alias per component) * **Optional Prefect workflows** for orchestration-heavy work * **Optional A2A or Pydantic AI agents** for agentic capabilities * **Your CI/CD pipeline** (per-repo, calling shared modules) ## Integration contracts Five contracts cross the platform-solution boundary. Get these right and the rest is implementation detail. ### 1. JWT (authentication) Every request to your service carries `Authorization: Bearer `. The token is issued by Keycloak in the user's organization realm. Validate the signature against Keycloak's JWKS, extract `org_id` from the issuer URL, and reject requests with no token. See [Authenticate Users](/guides/solution-dev/authenticate-users) for code. ### 2. `X-Project-ID` header (project scoping) JWT alone gives you organization context. The active project is communicated via `X-Project-ID`. Every database query, storage path, and downstream call must be scoped by both. See [Concepts](/getting-started/concepts) for the full multi-tenancy model. ### 3. Connections API (data sources) When your solution needs to talk to a customer database or object store, you do *not* ask the customer for credentials directly. They register a [Data Connection](/platform/data-connections) via Assets, and you fetch the configured connection at runtime. Credentials never live in your code or config. ### 4. obstore (shared storage) For artifacts, files, and intermediate data, use the platform's `obstore` abstraction. Same code works against S3, GCS, Azure Blob, and local MinIO — pick the backend with the configured connection. See [Use Shared Storage](/guides/solution-dev/use-shared-storage). ### 5. LiteLLM gateway (LLM access) Solutions never call OpenAI or Anthropic directly. Instead, point your OpenAI-compatible SDK at `LLM_GATEWAY_URL`. The gateway provides central key management, per-project cost attribution, rate-limit enforcement, and observability via Langfuse. See [Access LLMs](/guides/solution-dev/access-llms). ## What does NOT cross the boundary | You should never… | Because… | | ----------------------------------------------- | -------------------------------------------------------------------------- | | Run your own Keycloak | Multi-realm setup is platform infrastructure; rolling your own breaks SSO | | Write your own RBAC | OpenFGA is the platform's source of truth; permission decisions stay there | | Bake credentials into images or git | Use the secrets pipeline; rotation depends on it | | Add a foreign key from your DB to a platform DB | Breaks independent restore/backup; use API contracts instead | | Call OpenAI/Anthropic from your service code | LiteLLM gateway is the only attribution surface | | Access another solution's database directly | Cross-service joins are forbidden; use the other solution's API | ## Next steps Build a working hello-solution in 30 minutes. Naming, namespaces, image registries, registration steps. Iterate quickly with docker-compose, hot reload, and mocked platform services. Read the platform-side concepts (orgs, projects, X-Project-ID). # Authenticate Users Source: https://docs.emergence.ai/guides/solution-dev/authenticate-users Validate JWTs, use service accounts, and propagate X-Project-ID from your solution's services. # Authenticate Users This page shows how a solution validates user identity and project context. The platform provides Keycloak-issued JWTs (multi-realm OIDC/PKCE); your service is responsible for validating the signature, extracting claims, and propagating context to downstream calls. For the full platform-side authentication architecture (realm-per-org, JWKS rotation, SSO providers), see [Platform › Authentication](/platform/authentication) and [Security › Authentication](/security/authentication). ## Request flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% sequenceDiagram autonumber participant U as User / Client participant S as Your service participant KC as Keycloak (JWKS) participant G as Governance (OpenFGA) participant DB as Your DB U->>S: Request + Authorization: Bearer JWT + X-Project-ID S->>KC: GET /realms/<org>/protocol/openid-connect/certs (cached) KC-->>S: JWKS keys S->>S: Verify signature, exp, aud, iss S->>G: require_permission(user, action, resource) G-->>S: allow / deny alt Allowed S->>DB: Query scoped by org_id + project_id DB-->>S: Result S-->>U: 200 OK else Denied S-->>U: 403 Forbidden end ``` ## Steps Use `python-jose` to verify the token against the realm's JWKS. Cache the JWKS for \~10 minutes to avoid hammering Keycloak. ```python packages/api/src/api/auth.py theme={null} import os import time from typing import Annotated import httpx from fastapi import Depends, Header, HTTPException, status from jose import jwt, JWTError KEYCLOAK_ISSUER_URL = os.environ["KEYCLOAK_ISSUER_URL"] # e.g. https://kc.example.com/realms/ KEYCLOAK_AUDIENCE = os.environ["KEYCLOAK_AUDIENCE"] # your service's client_id # JWKS TTL — Keycloak rotates signing keys (default ~12h). Lifetime cache # (e.g. lru_cache) WILL produce 401s for valid tokens after rotation. _JWKS_TTL_SECONDS = 600 # 10 minutes _jwks_cache: dict | None = None _jwks_cached_at: float = 0.0 def _jwks() -> dict: global _jwks_cache, _jwks_cached_at now = time.monotonic() if _jwks_cache is None or now - _jwks_cached_at > _JWKS_TTL_SECONDS: url = f"{KEYCLOAK_ISSUER_URL.rstrip('/')}/protocol/openid-connect/certs" _jwks_cache = httpx.get(url, timeout=5.0).raise_for_status().json() _jwks_cached_at = now return _jwks_cache def current_user(authorization: Annotated[str | None, Header()] = None) -> dict: if not authorization or not authorization.lower().startswith("bearer "): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token") token = authorization.split(" ", 1)[1] try: claims = jwt.decode( token, _jwks(), algorithms=["RS256"], audience=KEYCLOAK_AUDIENCE, issuer=KEYCLOAK_ISSUER_URL, ) except JWTError as e: raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}") return claims def project_id(x_project_id: Annotated[str | None, Header()] = None) -> str: if not x_project_id: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing X-Project-ID header") return x_project_id ``` ```python packages/api/src/api/main.py theme={null} from fastapi import Depends, FastAPI from typing import Annotated from .auth import current_user, project_id app = FastAPI() @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/echo") async def echo( msg: str, user: Annotated[dict, Depends(current_user)], proj: Annotated[str, Depends(project_id)], ) -> dict[str, str]: return { "echo": msg, "user_sub": user["sub"], "org_id": user.get("org_id", ""), "project_id": proj, } ``` Routes that need auth declare `current_user` and (when project context matters) `project_id` as dependencies. Health probes stay unauthenticated. Standard claims you'll use: | Claim | Meaning | Notes | | -------- | ----------------- | ----------------------------------------------- | | `sub` | User UUID | Stable across sessions | | `iss` | Issuer URL | Realm path encodes `org_id` (last path segment) | | `aud` | Audience | Must equal your service's client\_id | | `exp` | Expiry | UNIX seconds | | `org_id` | Organization | Custom claim — also extractable from `iss` | | `groups` | Group memberships | Used by Governance for role mapping | Helper to extract `org_id` from the issuer when the custom claim isn't set: ```python theme={null} def org_id_from_issuer(iss: str) -> str: # iss like "https://kc.example.com/realms/acme-corp" return iss.rstrip("/").rsplit("/", 1)[-1] ``` Solutions never implement RBAC themselves — they delegate to OpenFGA via Governance. Forward the user's JWT and the resource ID; Governance returns allow/deny. ```python theme={null} import httpx, os GOVERNANCE_URL = os.environ.get("GOVERNANCE_URL", "http://em-runtime-governance.em-runtime:8000") async def require_permission(token: str, project_id: str, action: str, resource_uri: str) -> None: async with httpx.AsyncClient() as c: r = await c.post( f"{GOVERNANCE_URL}/governance/permissions/check", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, json={"action": action, "resource_uri": resource_uri}, timeout=5.0, ) r.raise_for_status() if not r.json()["allowed"]: raise HTTPException(status.HTTP_403_FORBIDDEN, "Not authorized") ``` Use the auto-generated [Python SDK](/sdks/python) instead of raw HTTP in real services — it handles error mapping, retries, and OpenAPI-validated request shapes. Local tests should not call real Keycloak. Generate a signed token with a fixture private key and load the matching public key as a JWKS override. ```python tests/conftest.py theme={null} from datetime import datetime, timedelta, timezone from jose import jwt import pytest PRIV_KEY = open("tests/fixtures/dev-private.pem").read() PUB_KEY = open("tests/fixtures/dev-public.pem").read() @pytest.fixture def dev_token(): now = datetime.now(timezone.utc) return jwt.encode( { "sub": "user-123", "iss": "http://localhost:8080/realms/dev", "aud": "hello-solution-api", "org_id": "dev", "groups": ["developers"], "iat": int(now.timestamp()), "exp": int((now + timedelta(hours=1)).timestamp()), }, PRIV_KEY, algorithm="RS256", ) ``` Generate the keypair once with `openssl`: ```bash theme={null} openssl genrsa -out tests/fixtures/dev-private.pem 2048 openssl rsa -in tests/fixtures/dev-private.pem -pubout -out tests/fixtures/dev-public.pem ``` Configure your `current_user` dependency to load `tests/fixtures/dev-public.pem` as JWKS in test mode (e.g., when `PYTEST_CURRENT_TEST` is set). ## Service-to-service auth When your solution's worker calls another service (or back into Governance/Assets), it can't use a user's JWT. Use a **service account**: a Keycloak client with `client_credentials` grant. See [Platform › Service Accounts](/platform/service-accounts) for the management API. ```python theme={null} async def service_token() -> str: async with httpx.AsyncClient() as c: r = await c.post( f"{KEYCLOAK_ISSUER_URL}/protocol/openid-connect/token", data={ "grant_type": "client_credentials", "client_id": os.environ["SVC_ACCOUNT_CLIENT_ID"], "client_secret": os.environ["SVC_ACCOUNT_SECRET"], }, timeout=5.0, ) return r.json()["access_token"] ``` The service-account client must be granted the same OpenFGA roles you'd grant a user. Cache tokens for `expires_in - 60` seconds to avoid thundering herds at expiry. ## Common errors See the auth section of [Troubleshooting](/guides/solution-dev/troubleshooting#auth). ## Next steps Move `KEYCLOAK_AUDIENCE` and DB creds out of `.env`. Configure OpenFGA roles for your solution's resources. Reference for all platform-API auth headers. Use the auto-generated SDK instead of raw HTTP. # Local Development Source: https://docs.emergence.ai/guides/solution-dev/local-development Iterate on your solution locally with hot reload, mocked platform services, and fast feedback before deploying to the cluster. # Local Development This page covers three local-dev modes — choose based on what you're iterating on. The [Quickstart](/guides/solution-dev/quickstart) uses Kind; this page also covers the docker-compose loop (faster) and port-forward to a shared dev cluster (slowest, but real platform). ## Pick a mode ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD Q{"What are you
iterating on?"} Q -->|"Service code only"| Q2{"Need real K8s
semantics?"} Q -->|"Platform integration
(identity + authz services)"| C["Port-forward to
shared dev cluster
(~30s per redeploy)"] Q2 -->|"No (probes mocked OK)"| A["docker-compose
+ uvicorn --reload
(<5s hot reload)"] Q2 -->|"Yes (probes, ingress,
configmaps)"| B["Kind cluster
+ kind-load.sh
(~30s per redeploy)"] classDef q fill:#F0E442,stroke:#555555,color:#000 classDef mode fill:#0072B2,stroke:#555555,color:#fff class Q,Q2 q class A,B,C mode ``` | Mode | Use when | Setup time | Iteration time | | -------------------------------------- | ---------------------------------------------------------------------------------------- | ---------- | ------------------ | | **docker-compose** | Iterating on your service code only; platform services can be mocked or stubbed | \~2 min | \<5s (hot reload) | | **Kind** | You need a real Kubernetes context (probes, configmaps, ingress) | \~5 min | \~30s per redeploy | | **Port-forward to shared dev cluster** | You need real platform services (real identity/authorization services, real Connections) | \<1 min | \~30s per redeploy | Most loops are docker-compose. Move to Kind for the integration-test pass. Use the shared dev cluster only when you need cross-service coordination. ## Mode 1: docker-compose with hot reload This is the daily-driver loop. Your service runs on the host with `uvicorn --reload`; supporting services (Postgres for your DB, MinIO for shared storage, a local identity provider) run in containers. ```yaml docker-compose.yaml theme={null} services: postgres: image: postgres:17-alpine environment: POSTGRES_USER: dev POSTGRES_PASSWORD: dev POSTGRES_DB: hello_solution ports: ["5432:5432"] healthcheck: test: ["CMD-SHELL", "pg_isready -U dev"] interval: 2s retries: 20 minio: image: minio/minio:latest command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: ["9000:9000", "9001:9001"] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] interval: 2s retries: 20 # Local identity provider for JWT validation. Uses the platform's standard # dev image so JWKS endpoints behave realistically. keycloak: image: quay.io/keycloak/keycloak:26.0 command: ["start-dev"] environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin ports: ["8080:8080"] ``` ```bash .env.example theme={null} # Database (matches docker-compose service) DATABASE_URL=postgresql+asyncpg://dev:dev@localhost:5432/hello_solution # Object storage (MinIO with path-style addressing) OBSTORE_ENDPOINT=http://localhost:9000 OBSTORE_ACCESS_KEY=minioadmin OBSTORE_SECRET_KEY=minioadmin OBSTORE_BUCKET=hello-solution OBSTORE_PATH_STYLE=true # Auth (point at the local identity provider realm you create with the bootstrap below) KEYCLOAK_ISSUER_URL=http://localhost:8080/realms/dev KEYCLOAK_AUDIENCE=hello-solution-api # LLM gateway (set to a real LiteLLM gateway URL in dev/stg/prod; # for local-only loops you can stub the LLM client) LLM_GATEWAY_URL=http://localhost:4000 LLM_GATEWAY_API_KEY=local-dev-key LLM_DEFAULT_MODEL=gemini-3.5-flash ``` Copy to `.env` (which should be in `.gitignore`) before running. ```bash theme={null} docker compose up -d docker compose ps # confirm all healthy # Pre-create the MinIO bucket docker run --rm --network host minio/mc \ sh -c "mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb -p local/hello-solution" # Run your service with hot reload set -a; source .env; set +a uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload ``` Editing `packages/api/src/api/*.py` reloads the server inside \~1 second. Hit `http://localhost:8000/echo?msg=hi` to confirm. ```bash theme={null} docker compose down -v # -v also removes the named volumes ``` ## Mode 2: Kind cluster Use Kind when you need real Kubernetes semantics (probes firing, ConfigMaps mounting, Ingress routing). The [Quickstart](/guides/solution-dev/quickstart) walks through this end-to-end. Make iteration faster with this rebuild-and-load script: ```bash kind-load.sh theme={null} #!/usr/bin/env bash set -euo pipefail CLUSTER="${1:-hello-solution}" TAG="${2:-dev}" docker build -t hello-solution-api:$TAG . kind load docker-image hello-solution-api:$TAG --name $CLUSTER # Trigger a rolling restart (image pull policy IfNotPresent + same tag won't redeploy) kubectl -n em-hello-solution rollout restart deployment/hello-solution-api kubectl -n em-hello-solution rollout status deployment/hello-solution-api ``` Run `./kind-load.sh` after each code change. \~30 seconds per cycle. ## Mode 3: Port-forward to shared dev cluster Use this when you need real platform behavior — real identity provider realms, real authorization data, real Connections. Run your code locally and forward platform services from a shared `dev` cluster. ```bash theme={null} # Connect to your shared dev cluster (context provisioned out of band by your platform team) kubectl config use-context # Port-forward platform services to localhost. # Each em-runtime service exposes Service port 8000; the local port is chosen for distinctness. kubectl -n em-runtime port-forward svc/em-runtime-governance 8001:8000 & kubectl -n em-runtime port-forward svc/em-runtime-assets 8002:8000 & kubectl -n em-runtime port-forward svc/em-runtime-utils 8003:8000 & kubectl -n em-runtime port-forward svc/keycloak 8080:8080 & # Run your service against them set -a; source .env.dev; set +a uv run uvicorn api.main:app --reload ``` Useful pitfall to know about: when `port-forward` drops (any error in the cluster), curl from your service will fail until you restart the forward. Wrap forwards in a tiny supervisor script (`while true; do kubectl port-forward ... ; done`) for long-running loops. ## Mocking platform services Three things are commonly mocked in unit tests: 1. **JWT verification** — generate a signed test token with a fixture private key; load the matching public key as `KEYCLOAK_JWKS_OVERRIDE` (a path to a JSON file your auth code prefers over the JWKS endpoint when set). 2. **Permission checks** — patch the Governance SDK's permission method to return `True` (or the value under test). 3. **LiteLLM gateway** — point `LLM_GATEWAY_URL` at a stub HTTP server that returns canned completions; or use `litellm`'s built-in mock provider (`model="mock-openai/..."`). See [SDKs › Python](/sdks/python) for SDK extension points and [Authenticate Users](/guides/solution-dev/authenticate-users) for the JWT fixture pattern. ## Common pitfalls Containers in Kind cannot use `localhost` to reach your host machine. Use `host.docker.internal` (macOS/Windows) or the host's LAN IP (Linux). Or — better — run dependencies inside Kind with another `helm install`. MinIO requires path-style addressing. Set `OBSTORE_PATH_STYLE=true` (or the equivalent in your S3 client config). Virtual-hosted style works on real S3 but not on MinIO without DNS magic. `uvicorn --reload` watches the working directory by default. If your source is in a non-default location (e.g., `packages/api/src/api/`), pass `--reload-dir packages/api/src`. For container-based reload, mount the source directory as a volume and run uvicorn inside the container. Every call to Governance, Assets, or Utils requires `X-Project-ID`. In local dev, hardcode a fixture project ID (e.g., `00000000-0000-0000-0000-000000000001`) until you wire user-driven project switching. ## CI smoke test The [Quickstart](/guides/solution-dev/quickstart) runs end-to-end in CI weekly via `.github/workflows/quickstart-smoke.yml`. The workflow provisions a Kind cluster, builds the example image, installs the chart, port-forwards, and asserts both endpoints respond. If it fails, an issue is opened automatically with the label `solution-dev-guide` — that is your signal to update the guide. This is the strongest possible self-sustainability signal: the quickstart cannot rot silently. ## Next steps Wire JWT validation into your local app. Move secrets out of `.env` into the platform pipeline. Replace MinIO test code with the obstore client. Stuck? Check known causes by error symptom. # Manage Secrets Source: https://docs.emergence.ai/guides/solution-dev/manage-secrets Retrieve secrets at runtime — environment injection via em-service, file mounts at /mnt/secrets/, and adding new secrets to your chart. # Manage Secrets This page is about how a *solution* gets secrets at runtime. The platform handles sourcing, rotation, and delivery — your job is to declare what you need and read it correctly. For the platform-side architecture (Infisical vs ESO + GCP Secret Manager, rotation mechanics, Workload Identity), see [Security › Secrets Management](/security/secrets-management) and [Deployment › Infrastructure › Secrets](/deployment/infrastructure/secrets). ## Secret pipeline ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR subgraph Upstream["Secret backend (platform-managed)"] BACKEND[("Infisical
or
ESO + GCP SM")] end subgraph Cluster["Your namespace (em-<solution>)"] K8S[("K8s Secret
<solution>-secrets")] POD["Your pod"] ENV["env vars
(via envFrom)"] FILE["files at
/mnt/secrets/<key>"] end RELOAD["Stakater Reloader"] BACKEND -->|"sync (~1h)"| K8S K8S --> ENV K8S --> FILE ENV --> POD FILE --> POD K8S -.->|"on change"| RELOAD RELOAD -.->|"rolling restart"| POD classDef store fill:#009E73,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef act fill:#E69F00,stroke:#555555,color:#000 classDef sol fill:#56B4E9,stroke:#555555,color:#000 class BACKEND,K8S store class RELOAD infra class ENV,FILE act class POD sol ``` **Read the diagram left-to-right**: the upstream backend is the source of truth; ESO or Infisical syncs values into a K8s Secret in your namespace; `em-service` injects them into your pod as env vars (via `envFrom`) and/or files (via volume mount). On rotation, Reloader detects the K8s Secret change and rolls your Deployment. ## Two delivery shapes The `em-service` chart delivers secrets to your pod in two ways. Pick whichever fits the consumer: | Shape | Format | Best for | | ------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | **Environment variable** | Loaded via `envFrom` from a synthesized `-secrets` Kubernetes Secret | Most use cases (DB URLs, API keys, OIDC client secrets) | | **File mount** | Files at `/mnt/secrets/` | Multi-line secrets (PEM keys, certs); SDKs that expect a file path (gcloud, kubeconfig) | Both are populated from the *same* upstream secret backend; the difference is purely how your code reads the value. ## Steps Reference the secret by `secretKeyRef` in your `values.yaml`. The Kubernetes Secret itself (`-secrets`) is created by the platform's secret pipeline — you do not check secret values into the chart. ```yaml charts//values.yaml theme={null} api: env: # Plain values: use the simple form LOG_LEVEL: "INFO" KEYCLOAK_ISSUER_URL: "https://keycloak.example.com/realms/acme" KEYCLOAK_AUDIENCE: "-api" envVars: # Secret values: use envVars with valueFrom.secretKeyRef - name: DATABASE_URL valueFrom: secretKeyRef: name: -secrets key: database-url - name: LLM_GATEWAY_API_KEY valueFrom: secretKeyRef: name: -secrets key: llm-gateway-api-key ``` For file-mount secrets, declare a volume and mount it (the platform's `em-service` chart supports custom mounts via `volumeMounts` and `volumes`): ```yaml theme={null} api: volumeMounts: - name: tls mountPath: /mnt/secrets/tls readOnly: true volumes: - name: tls secret: secretName: -tls ``` Environment variables: standard `os.environ`. File mounts: read from disk lazily. ```python theme={null} import os from pathlib import Path # Env-injected secret db_url = os.environ["DATABASE_URL"] # File-mounted secret tls_cert = Path("/mnt/secrets/tls/tls.crt").read_text() tls_key = Path("/mnt/secrets/tls/tls.key").read_text() ``` Two rules: 1. **Never log a secret value.** Wrap reads in repr-safe types or use `pydantic.SecretStr`. 2. **Don't re-read on every request.** Read once at startup; re-read only when a rotation signal fires (see Stakater Reloader below). Adding a secret requires two changes: 1. **Add it to your chart** (the `envVars` snippet above) referencing the key name. 2. **Add the value to the upstream secret backend** in each environment. The upstream backend depends on where you're deploying: | Deployment | Where to add the secret | | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | Local dev (docker-compose / Kind) | `.env` (gitignored) or `kubectl create secret generic -secrets --from-literal=key=value` | | Self-hosted (Infisical) | Infisical UI for the project; ESO syncs to the cluster | | Cloud (GCP Secret Manager via ESO) | `gcloud secrets create` + grant Workload Identity SA `secretmanager.secretAccessor` | See [Security › Secrets Management](/security/secrets-management) and [Deployment › Infrastructure › Secrets](/deployment/infrastructure/secrets) for backend setup. For local-dev secrets: * **Use `.env`** (in `.gitignore`) for env vars; `set -a; source .env; set +a` before `uv run`. * **Mount fixture files** for file-style secrets via docker-compose `volumes`. * **Never commit secrets** — even fixture/dummy ones if they look real. Use values like `dev-only-not-a-real-key`. * **Generate fresh keys** for tests (e.g., the `dev-private.pem` from [Authenticate Users](/guides/solution-dev/authenticate-users)). ## Where the values come from Solutions don't talk to the secret backend directly — the platform's secret pipeline ([ESO](/deployment/infrastructure/secrets) for cloud, [Infisical](/security/secrets-management) for self-hosted) materializes upstream secrets into Kubernetes Secrets in your namespace. Your chart's `secretKeyRef` references those K8s Secrets. The platform handles: * Authentication to the upstream backend (Workload Identity in GCP, machine identities in Infisical) * Sync intervals (ESO defaults to 1h; configurable per ExternalSecret) * Resync on upstream change You do not see the upstream backend from your service code. Just declare the K8s Secret and key. ## Rotation The platform pairs secrets with [Stakater Reloader](https://github.com/stakater/Reloader). When the underlying K8s Secret changes (because ESO synced an update from upstream), Reloader rolls your Deployment automatically — *if* you opt in by annotating the Deployment. `em-service` adds the annotation by default for the secrets it injects via `envFrom`. For custom volume mounts (like `-tls`), add the annotation manually: ```yaml charts//values.yaml theme={null} api: podAnnotations: secret.reloader.stakater.com/reload: "-tls" ``` Verify the rollout fires: ```bash theme={null} # Update the upstream secret, then watch kubectl -n em- rollout status deployment/-api ``` ## Verification ```bash theme={null} # Confirm the K8s Secret exists with the expected keys kubectl -n em- get secret -secrets -o jsonpath='{.data}' | jq 'keys' # Confirm the env var reaches the pod kubectl -n em- exec deployment/-api -- printenv DATABASE_URL | head -c 60; echo # Confirm the file mount reaches the pod (if applicable) kubectl -n em- exec deployment/-api -- ls -la /mnt/secrets/tls ``` If the secret is declared but the pod fails with `secret not found`: ESO/Infisical hasn't synced yet, or the upstream secret doesn't exist. See [Troubleshooting › secrets](/guides/solution-dev/troubleshooting#secrets). ## Next steps Reference for `env` vs `envVars` precedence and dedup rules. Storage credentials follow the same pattern as DB creds. Platform-side architecture (ESO vs Infisical). Pod CrashLoopBackOff with "secret not found" and similar. # Solution Developer Guide Source: https://docs.emergence.ai/guides/solution-dev/overview Build, package, and deploy a new solution on the CRAFT platform — from first FastAPI service to production deployment. **For AI agents and LLMs**: this site is Mintlify-hosted, so every page is also available as Markdown by appending `.md` to the URL (or via content negotiation with `Accept: text/markdown`). Prefer those variants — HTML wastes context tokens. The site-wide index lives at `/llms.txt` and the full corpus at `/llms-full.txt`. **Cursor / Claude Code / Windsurf users** — connect your IDE to `https://docs.emergence.ai/mcp` for live doc queries during coding tasks (MCP search tool enabled). # Solution Developer Guide This guide walks you through building a new **solution** on the CRAFT platform: registering it, wiring it to platform services (auth, secrets, storage, LLMs), running it locally, and deploying it. It is the single starting point for engineers building on top of CRAFT. ## Who this is for This guide is for **engineers building a new solution on CRAFT** — Emergence platform engineers, embedded teams, and co-design partners. It is *not* for end users of an already-built solution — those have their own docs: [Data Insights](/data-insights/overview), [Data Governance](/data-governance/overview), and [Semiconductor](/semiconductor/overview). ### Reader audiences and where you read There are two reader sites: | Site | URL | You see | | -------- | ------------------------------------------------- | ------------------------------------------------------------ | | Internal | `docs-internal.emergence.ai` (password-protected) | Everything, including internal-only pages and gated sections | | External | `docs.emergence.ai` (public) | Only the partner-safe content | If you are reading on the public site (`docs.emergence.ai`), some implementation-specific sections are hidden by design — the partner-friendly capability descriptions remain. ## Journey map ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR A["Read overview"] --> B["Quickstart
(30 min)"] B --> C["Understand
architecture"] C --> D["Local dev"] D --> E["Register your
solution"] E --> F["Wire integrations"] F --> G["Package &
deploy"] G --> H["Operate"] F -.-> F1["Auth"] F -.-> F2["Secrets"] F -.-> F3["Storage"] F -.-> F4["LLMs"] classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef step fill:#0072B2,stroke:#555555,color:#fff classDef leaf fill:#E69F00,stroke:#555555,color:#000 class A entry class B,C,D,E,F,G,H step class F1,F2,F3,F4 leaf ``` ## Use this when… You want a working hello-solution in 30 minutes. You want to understand what the platform provides and what your solution owns. You're iterating on your service locally and want hot reload + mocked platform services. You're starting a new solution and need names, namespaces, and registration steps. You need to validate JWTs and propagate `X-Project-ID` from your service. You need to inject secrets into pods and rotate them safely. You need to read and write objects (artifacts, files) to S3/GCS/Azure/MinIO. Your solution calls LLMs and you need provider-agnostic, observable, cost-attributed access. You're ready to build a multi-component Helm chart and ship it to a cluster. You want copy-pasteable scaffolds for three common solution shapes. Something is broken and you want a probable cause + fix in seconds. ## Issue-to-page traceability This guide addresses the [seven onboarding requirements](#issue-to-page-traceability) tracked internally as follows: | Requirement | Primary page(s) | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Register a new solution | [Register a solution](/guides/solution-dev/register-a-solution) | | Container & namespace conventions | [Register a solution](/guides/solution-dev/register-a-solution); [Package and deploy](/guides/solution-dev/package-and-deploy) | | Connect to Keycloak (auth) | [Authenticate users](/guides/solution-dev/authenticate-users) | | Connect to Infisical (secrets) | [Manage secrets](/guides/solution-dev/manage-secrets) | | Connect to shared storage | [Use shared storage](/guides/solution-dev/use-shared-storage) | | LLM integration | [Access LLMs](/guides/solution-dev/access-llms) | | Local development setup | [Local development](/guides/solution-dev/local-development) | | Minimal working example | [Quickstart](/guides/solution-dev/quickstart); [Starter templates](/guides/solution-dev/starter-templates) Tier 1 | | Starter templates for common scenarios | [Starter templates](/guides/solution-dev/starter-templates) | ## Reference solutions Three production solutions live on the platform today; treat them as canonical examples when you need a working pattern. | Solution | Code repo | Pattern | | -------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------- | | [Data Insights](/data-insights/overview) | `em-talk2data` | Multi-agent A2A protocol — gateway service + insights agent + text2sql agent + LiteLLM | | [Data Governance](/data-governance/overview) | `em-data-readiness` | API service + Prefect workflows — profiling, enrichment, DQ rules | | [Semiconductor](/semiconductor/overview) | `em-semi` | Pydantic AI agents + air-gapped deployment — fab analytics | The starter templates page maps each tier to one of these reference solutions: Tier 1 ≈ minimal API service, Tier 2 ≈ `em-data-readiness` shape, Tier 3 ≈ `em-talk2data` shape. ## Success criteria — your feedback closes the loop This guide has one explicit performance target: > **The [Quickstart](/guides/solution-dev/quickstart) must take you 30 minutes or less, end to end.** If it takes longer, that is a guide bug — please report it via the 👎 thumbs at the bottom of the affected page (Mintlify captures the comment and routes it to the docs team). Use the 👍 / 👎 thumbs at the bottom of every page. Thumbs-down comments roll up into the quarterly docs governance review for this group. The quickstart also runs in CI weekly (see [Local development](/guides/solution-dev/local-development#ci-smoke)) so we know within a week if it stops working. ## Conventions used in this guide * **Placeholders**: `` is your solution's name (DNS-safe, lowercase, hyphens). `` is one of `dev`, `stg`, `prod`. `` and `` come from your JWT and `X-Project-ID` header — see [Concepts](/getting-started/concepts). * **Pinned versions**: Code samples are pinned to **em-service v0.0.15** (the version in [em-service Chart](/deployment/helm/em-service) as of 2026-05-14). Major em-service version bumps refresh the Tier 1 fixtures under `scripts/tests/fixtures/solution-dev/tier1/`; `npm run check-snippets` (CI) fails if inline snippets drift. * **Service catalog**: There is no Backstage-style service catalog yet; the closest current surface is the em-runtime Assets API where you register agents, data connections, and artifacts — see [Register Your First Agent](/guides/first-agent). A first-class portal is on the platform roadmap. * **Cross-refs**: Use absolute paths (`/guides/...`, `/platform/...`). * **Diagrams**: Mermaid only, with the project init block and the Okabe-Ito palette (entry `#56B4E9`, infrastructure `#0072B2`, data store `#009E73`, action `#E69F00`, decision `#F0E442`, external `#CC79A7`). ## What's next If you are reading this for the first time, go straight to the [Quickstart](/guides/solution-dev/quickstart). If you have a running hello-solution and want to add real integrations, jump to the relevant how-to from the journey map above. # Package and Deploy Source: https://docs.emergence.ai/guides/solution-dev/package-and-deploy Wrap a multi-component solution in a Helm chart, manage per-environment overlays, and ship the image to your registry. # Package and Deploy The [Quickstart](/guides/solution-dev/quickstart) deploys a single-component solution. This page extends to multi-component (api + worker + scheduler + agent…), per-environment overlays, image-publish workflows, and where ArgoCD picks up from there. ## Single vs multi-component `em-service` (v0.0.15) is the base chart for *one* deployable workload — one Deployment, one Service, one set of probes. Solutions that need more than one workload include `em-service` multiple times as subchart dependencies, each with a unique `alias`. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB subgraph Parent["Your chart: charts/<solution>"] CHART["Chart.yaml
(declares subcharts)"] VAL["values.yaml
+ values.<env>.yaml"] end subgraph EM["em-service v0.0.15 (per alias)"] API["alias: api
Deployment + Service + probes"] WORKER["alias: worker
Deployment + probes"] SCHED["alias: scheduler
Deployment (singleton)"] end CHART --> API CHART --> WORKER CHART --> SCHED VAL -->|"merge: defaults +
values.<env>.yaml"| API VAL --> WORKER VAL --> SCHED classDef parent fill:#56B4E9,stroke:#555555,color:#000 classDef act fill:#E69F00,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff class CHART,VAL parent class API,WORKER,SCHED infra ``` **Read top-down**: one umbrella chart per solution; one `em-service` instance per workload (api / worker / scheduler / agent); per-environment values overlay-merge with the defaults. Add a fourth alias and you've added a fourth deployable component — no other moving parts change. See [em-service Chart › Using as a subchart](/deployment/helm/em-service#using-as-a-subchart) for the canonical reference. ## Multi-component layout A typical "API + worker" solution: ```yaml charts//Chart.yaml theme={null} apiVersion: v2 name: description: on CRAFT type: application version: 0.1.0 appVersion: "0.1.0" dependencies: - name: em-service alias: api version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts - name: em-service alias: worker version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts - name: em-service alias: scheduler # optional Prefect / cron component version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts ``` Each alias has its own values block in `values.yaml`: ```yaml charts//values.yaml theme={null} api: image: repository: ghcr.io//-api tag: 0.1.0 service: type: ClusterIP port: 8000 env: LOG_LEVEL: "INFO" envVars: - name: DATABASE_URL valueFrom: { secretKeyRef: { name: -secrets, key: database-url } } readinessProbe: { httpGet: { path: /healthz, port: 8000 } } livenessProbe: { httpGet: { path: /healthz, port: 8000 } } autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 70 worker: image: repository: ghcr.io//-worker tag: 0.1.0 service: enabled: false # workers don't expose HTTP env: LOG_LEVEL: "INFO" QUEUE_NAME: "-jobs" envVars: - name: DATABASE_URL valueFrom: { secretKeyRef: { name: -secrets, key: database-url } } scheduler: image: repository: ghcr.io//-scheduler tag: 0.1.0 service: enabled: false replicaCount: 1 # singleton — never scale schedulers horizontally ``` ## Per-environment overlays Use one `values.yaml` for shared defaults, plus `values..yaml` overlays for things that change between environments (image tags, replica counts, host names, log levels). Apply with `helm -f`: ```bash theme={null} helm upgrade --install ./charts/ \ --namespace em- --create-namespace \ --values ./charts//values.yaml \ --values ./charts//values.dev.yaml \ --wait --timeout 5m ``` Example dev overlay: ```yaml charts//values.dev.yaml theme={null} api: image: { tag: dev } replicaCount: 1 autoscaling: { enabled: false } env: { LOG_LEVEL: "DEBUG" } worker: image: { tag: dev } replicaCount: 1 ``` Production overlay (just the deltas): ```yaml charts//values.prod.yaml theme={null} api: image: { tag: 1.4.7 } autoscaling: { minReplicas: 3, maxReplicas: 20 } ingress: enabled: true className: nginx hosts: - host: .example.com paths: [{ path: /, pathType: Prefix }] worker: image: { tag: 1.4.7 } replicaCount: 4 ``` ## Build and publish images Per-component, multi-stage `Dockerfile` (one image per workload). Use the same Dockerfile parametrized via `--target` if your components share a build base; or one Dockerfile per component if they diverge significantly. ```bash theme={null} # Build docker build -t ghcr.io//-api:1.4.7 --target api . docker build -t ghcr.io//-worker:1.4.7 --target worker . # Login (whatever registry you use) docker login ghcr.io -u -p $GITHUB_TOKEN # Publish docker push ghcr.io//-api:1.4.7 docker push ghcr.io//-worker:1.4.7 ``` For private registries, configure `imagePullSecrets` in `values.yaml`: ```yaml theme={null} api: imagePullSecrets: [{ name: ghcr-pull }] worker: imagePullSecrets: [{ name: ghcr-pull }] ``` ## Versioning Keep the Helm chart `version` and the image `tag` independent: | What | Bumped when | | --------------------------------- | --------------------------------------------------------------------- | | Chart `version` (in `Chart.yaml`) | Chart structure changes (added/removed/renamed values, alias changes) | | `appVersion` | Default app version this chart targets — informational | | Image `tag` (in `values.yaml`) | Application code changes | Use [semver](https://semver.org/) for both. See [Helm › Upgrades](/deployment/helm/upgrades) for the upgrade policy (which chart-version bumps are breaking, how to roll back, etc.). ## Deploy ### Local ```bash theme={null} helm dependency update ./charts/ helm upgrade --install ./charts/ \ --namespace em- --create-namespace \ -f ./charts//values.yaml \ -f ./charts//values.dev.yaml \ --wait --timeout 5m kubectl -n em- get pods -w ``` Verify all components are running and probes are passing. ### CI/CD For automated deploys, use whatever CI/CD your team prefers; the chart itself is environment-agnostic. ## Air-gapped deployments If the target cluster has no internet access (some customer environments), pre-stage: 1. The chart tarball (`helm pull oci://ghcr.io/emergenceai/em-charts/em-service --version 0.0.15`) 2. All component images (`docker save | docker load` on the air-gapped side) 3. The pull-secret pointing at the customer's internal registry ## Verification ```bash theme={null} # Chart renders cleanly with the production overlay helm template ./charts/ \ -f ./charts//values.yaml \ -f ./charts//values.prod.yaml | head -50 # All declared components have running pods kubectl -n em- get deployments kubectl -n em- get pods # Image tags match what you intended to deploy kubectl -n em- get pods -o jsonpath='{.items[*].spec.containers[*].image}' | tr ' ' '\n' | sort -u ``` ## Next steps Full values reference for the base chart. Platform-wide chart configuration. Upgrade and rollback policy. Pre-built scaffolds for common solution shapes. # Quickstart: Hello Solution Source: https://docs.emergence.ai/guides/solution-dev/quickstart Stand up a minimal FastAPI service, package it as a Helm chart wrapping em-service, and deploy it to a local Kind cluster — under 30 minutes. # Quickstart: Hello Solution By the end of this tutorial you will have a FastAPI service running in a local Kubernetes cluster, packaged as a Helm chart that wraps the platform's `em-service` base chart, and reachable by `curl`. Total time: **30 minutes or less**. If it takes longer, that's a guide bug — please report via the 👎 thumbs at the bottom of this page. This tutorial keeps every command and config snippet inline. Its one external dependency is the platform's `em-service` base chart, pulled from Emergence's **private** container registry during install — registry access is granted to the Emergence team and Solution/Agent design partners (you authenticate with `helm registry login` in the install step). ## Prerequisites Install these tools. Versions are floors, not pins — newer is fine. | Tool | Version | Purpose | | ------- | ------- | ------------------------------ | | Docker | 24+ | Container runtime for Kind | | Kind | 0.20+ | Local Kubernetes cluster | | kubectl | 1.28+ | Kubernetes CLI | | Helm | 3.13+ | Package manager for Kubernetes | | uv | 0.4+ | Python project manager | ```bash theme={null} brew install docker kind kubectl helm uv ``` ```bash theme={null} # Docker — follow distro-specific instructions at https://docs.docker.com/engine/install/ # Kind curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind # kubectl curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" chmod +x kubectl && sudo mv kubectl /usr/local/bin/ # Helm curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash # uv curl -LsSf https://astral.sh/uv/install.sh | sh ``` ## What you'll build A solution called **`hello-solution`** with one component (`api`) that exposes: * `GET /healthz` → `{"status":"ok"}` for k8s probes * `GET /echo?msg=...` → echoes the message back The image is built locally and loaded into Kind (no registry needed). The Helm chart wraps `em-service` v0.0.15 with `alias: api`, so you'll see how multi-component solutions extend (just add another alias). ## Steps Pick a working directory and scaffold these files: ```text theme={null} hello-solution/ ├── packages/api/ │ ├── pyproject.toml │ └── src/api/ │ └── main.py ├── Dockerfile └── charts/hello-solution/ ├── Chart.yaml └── values.yaml ``` ```bash theme={null} mkdir -p hello-solution/packages/api/src/api hello-solution/charts/hello-solution cd hello-solution ``` ```python packages/api/src/api/main.py theme={null} from fastapi import FastAPI app = FastAPI(title="hello-solution") @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/echo") async def echo(msg: str = "hi") -> dict[str, str]: return {"echo": msg} ``` ```toml packages/api/pyproject.toml theme={null} [project] name = "hello-solution-api" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.110", "uvicorn[standard]>=0.30", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/api"] ``` A multi-stage build keeps the runtime image small. ```dockerfile Dockerfile theme={null} FROM python:3.12-slim AS build WORKDIR /app RUN pip install --no-cache-dir uv COPY packages/api/pyproject.toml packages/api/pyproject.toml COPY packages/api/src packages/api/src RUN uv pip install --system --no-cache-dir ./packages/api FROM python:3.12-slim COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn EXPOSE 8000 CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` The chart declares one `em-service` subchart, aliased as `api`. The platform's `em-service` (v0.0.15) handles Deployment, Service, probes, env vars — you only configure your image and ports. To add a worker later, you'd add another `em-service` entry with `alias: worker` and configure it under that alias key in `values.yaml`. See [em-service Chart](/deployment/helm/em-service) for the full values reference. ```yaml charts/hello-solution/Chart.yaml theme={null} apiVersion: v2 name: hello-solution description: A minimal CRAFT solution type: application version: 0.1.0 appVersion: "0.1.0" dependencies: - name: em-service alias: api version: 0.0.15 # latest tag published to the OCI registry repository: oci://ghcr.io/emergenceai/em-charts ``` **Why `0.0.15` and not the latest source version?** The em-charts source `Chart.yaml` is on `0.0.16` (and an unrelated `0.1.0` ships for `em-charts-internal/em-service-internal`), but the OCI tag `ghcr.io/emergenceai/em-charts/em-service:0.0.15` is the most recent version actually published to the registry — `0.0.16` returns "not found" on `helm pull`. em-talk2data's own `Chart.yaml` pins `0.0.15` for the same reason. When em-charts publishes a newer tag, bump here. ```yaml charts/hello-solution/values.yaml theme={null} api: # em-service's fullname template defaults to the release name, which would # name every component "hello-solution" — collide if you add another alias. # Set fullnameOverride per component so resources are named "-" # (this is what em-runtime itself does for governance/assets/utils). fullnameOverride: hello-solution-api image: repository: hello-solution-api tag: dev pullPolicy: IfNotPresent service: type: ClusterIP port: 8000 env: LOG_LEVEL: "INFO" readinessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 2 livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 5 ``` ```bash theme={null} # Create cluster kind create cluster --name hello-solution # Build and load image docker build -t hello-solution-api:dev . kind load docker-image hello-solution-api:dev --name hello-solution ``` `kind load docker-image` makes your local image available inside the cluster without a registry. ```bash theme={null} # Authenticate to the private em-charts registry first (team / design-partner access) helm registry login ghcr.io helm dependency update ./charts/hello-solution helm upgrade --install hello-solution ./charts/hello-solution \ --namespace em-hello-solution --create-namespace \ --wait --timeout 5m ``` First install pulls `em-service` from `ghcr.io/emergenceai/em-charts`, a **private** registry — `helm registry login ghcr.io` authenticates you (access is granted to the Emergence team and Solution/Agent design partners). ```bash theme={null} kubectl port-forward -n em-hello-solution svc/hello-solution-api 8000:8000 & curl -s "http://localhost:8000/healthz" | jq . # {"status":"ok"} curl -s "http://localhost:8000/echo?msg=ship%20it" | jq . # {"echo":"ship it"} ``` If both calls succeed, **you have shipped your first CRAFT solution**. ```bash theme={null} kill %1 2>/dev/null # stop port-forward helm uninstall hello-solution -n em-hello-solution kubectl delete namespace em-hello-solution kind delete cluster --name hello-solution ``` ## What you just did ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR SRC["Source
(packages/api/)"] DOCK["docker build
hello-solution-api:dev"] LOAD["kind load
docker-image"] HELM["helm install
(em-service v0.0.15)"] NS[("Namespace
em-hello-solution")] POD["Pod: api
(port 8000)"] PF["kubectl
port-forward"] CURL["curl /echo"] SRC --> DOCK --> LOAD --> HELM HELM --> NS NS --> POD POD --> PF --> CURL classDef sol fill:#56B4E9,stroke:#555555,color:#000 classDef act fill:#E69F00,stroke:#555555,color:#000 classDef store fill:#009E73,stroke:#555555,color:#000 class SRC,POD sol class DOCK,LOAD,HELM,PF,CURL act class NS store ``` You wrapped a 25-line FastAPI app in a Helm chart that depends on the platform's `em-service` base chart, deployed it to a namespace named `em-` (the platform convention), and reached it via `port-forward`. You did not configure auth, secrets, storage, or LLMs yet — those are the next how-tos: Make this naming + namespace pattern systematic. Protect `/echo` with JWT validation. Wire a secret-backed env var into your chart. Loop faster with docker-compose + hot reload. # Register a New Solution Source: https://docs.emergence.ai/guides/solution-dev/register-a-solution End-to-end checklist for registering a new solution on the platform — name, namespace, image registry, Helm wiring, database, and platform integration. # Register a New Solution This is the canonical checklist for starting a new solution on CRAFT. Follow these steps when you're past the [Quickstart](/guides/solution-dev/quickstart) prototype and ready to make the solution real. Most decisions here are conventions — the platform doesn't *force* you, but solutions that follow the conventions get free deployment, observability, and CI integration. ## Registration sequence ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR NAME["1. Pick a name
<solution>"] NS[("2. Create namespace
em-<solution>")] REG["3. Choose image
registry"] HELM["4. Wire Helm chart
(em-service alias)"] DB[("5. Provision DB
+ Alembic")] AG{"6. Has agents?"} AG_Y["Register via
Assets API"] CONN{"7. Has data
connections?"} CONN_Y["Register via
Assets API"] INT["8. Wire integrations
(auth/secrets/storage/LLMs)"] DONE([Solution live]) NAME --> NS --> REG --> HELM --> DB --> AG AG -->|yes| AG_Y --> CONN AG -->|no| CONN CONN -->|yes| CONN_Y --> INT CONN -->|no| INT INT --> DONE classDef act fill:#E69F00,stroke:#555555,color:#000 classDef store fill:#009E73,stroke:#555555,color:#000 classDef q fill:#F0E442,stroke:#555555,color:#000 classDef sol fill:#56B4E9,stroke:#555555,color:#000 class NAME,REG,HELM,AG_Y,CONN_Y,INT act class NS,DB store class AG,CONN q class DONE sol ``` The dashed sequence is the **order**, not the schedule — most solutions land steps 1–4 in one PR, 5 in the next, and 6/7/8 incrementally. Steps 6 and 7 are conditional; only Tier 3 agentic solutions need them up front. ## Pre-flight checklist Lowercase, alphanumeric + hyphens only, ≤ 30 chars. Used as the Helm release name, the namespace suffix (`em-`), and the package prefix in your monorepo. One repo per solution. Monorepo layout from day one — even with a single component — so adding a worker later is a 3-step diff. See [Architecture for Solution Developers](/guides/solution-dev/architecture-for-solution-developers#what-your-solution-owns). Any OCI registry your target cluster can pull from. Internal solutions use `ghcr.io/emergenceai/`. Partners typically use their own registry. Your own Postgres database. No cross-service foreign keys. Migrations versioned with Alembic from day one. Agents, data connections, files, and models registered through the platform's Assets API are discoverable to other solutions and the UI. Decide upfront what your solution exposes. ## Steps Match the solution name across: * Repo name (e.g. `em-talk2data`) * Helm release name (e.g. `talk2data`) * Namespace (e.g. `em-talk2data`) * Database (e.g. `talk2data`) Existing examples: `em-talk2data` (Data Insights), `em-data-readiness` (Data Governance), `em-semi` (Semiconductor). Stick to this rhythm — it makes ops searches and grep-based audits trivial. | Namespace | Used by | | ---------------------- | ------------------------------------------------------------------ | | `em-runtime` | Platform services (Governance, Assets, Utils, Keycloak, OpenFGA) | | `em-` | Your solution's pods, services, and config | | `em--system` | Cluster-scoped sidecars or operators owned by your solution (rare) | Create the namespace explicitly with a label so the platform's NetworkPolicy / Reloader / cost-attribution pick it up: ```bash theme={null} kubectl create namespace em- kubectl label namespace em- emergence.ai/solution= ``` Configure `image.repository` in your chart's `values.yaml`. For private registries, also configure `imagePullSecrets`: ```yaml theme={null} api: image: repository: /-api tag: 0.1.0 imagePullSecrets: - name: ``` Single component? See [Quickstart Step 4](/guides/solution-dev/quickstart). Multi-component (api + worker + scheduler + agent…)? See [Package and Deploy](/guides/solution-dev/package-and-deploy) for the alias pattern. The minimum is one `em-service` subchart per component, with each component aliased so its values live under that key: ```yaml charts//Chart.yaml theme={null} dependencies: - { name: em-service, alias: api, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts } - { name: em-service, alias: worker, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts } ``` Your solution owns its database. Two non-negotiable rules: 1. **No cross-service foreign keys.** Each service must be backup-restorable independently. Cross-service consistency is by API contract, not joins. 2. **Migrations from day one** — Alembic is the platform standard. Migrations are immutable once committed; never edit a published migration, write a new one. Provision via your chart (the Postgres subchart pattern) or via Crossplane claims for managed databases. Connection string injected as a secret-backed env var (see [Manage Secrets](/guides/solution-dev/manage-secrets)). If your solution exposes A2A agents (the Data Insights pattern), register each with the Assets API so other services and the UI can discover them. Follow [Register Your First Agent](/guides/first-agent) — that walkthrough covers Agent Card authoring (`/.well-known/agent-card.json`), validation, registration, listing, and update with optimistic concurrency. If your solution reads customer data (databases, object stores, file shares), the customer registers a [Data Connection](/platform/data-connections) and grants your solution permission. Your code fetches the connection at runtime — credentials never live in your config. Step through these in order: Validate JWTs and propagate `X-Project-ID`. Inject runtime secrets via the chart. Use obstore for cross-cloud blob access. Call the LiteLLM gateway (never providers directly). ## What you get when you follow the conventions | If you… | You get… | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Follow `em-` namespace | Auto-applied NetworkPolicy, Reloader watching your Secrets, cost attribution by solution | | Wrap `em-service` with one alias per component | Standard probes, env var deduplication, Gateway API ingress, HPA — all configurable but with sane defaults | | Register agents in Assets | Discoverability from the UI shell + other solutions; permission integration with OpenFGA | | Use Connections for data sources | Credential rotation handled by the platform; never in your code | | Call LLMs via LiteLLM gateway | Per-project cost attribution, rate limits enforced centrally, traces in Langfuse | ## Verification ```bash theme={null} # Namespace label set kubectl get namespace em- -o jsonpath='{.metadata.labels}' # Helm release lives in the right place helm list -n em- # Image pulls succeed (no ImagePullBackOff) kubectl -n em- get pods # DB migration ran (if applicable) kubectl -n em- logs job/-migrate ``` If anything is unhealthy, see [Troubleshooting](/guides/solution-dev/troubleshooting). ## Next steps Multi-component charts, env overlays, image publish. Test changes locally before pushing. The first integration most solutions need. Common errors at registration time. # Starter Templates Source: https://docs.emergence.ai/guides/solution-dev/starter-templates Copy-paste scaffolds for three common solution shapes — minimal API service, multi-component (api + worker + DB), and agentic A2A solution. # Starter Templates Three tiers, increasing in complexity. Each tier is a starting point — copy, then mutate to fit. The Tier 1 fixtures are mirrored in `scripts/tests/fixtures/solution-dev/tier1/` and verified by `npm run check-snippets` so this page does not silently rot. **Future**: when the `em-service-template` repo lands, these tiers collapse to `cookiecutter gh:emergenceai/em-service-template --no-input solution_name= tier=<1|2|3>`. The inline scaffolds here are the bridge until that ships. Every scaffold pins `em-service` to **v0.0.15** (the version in [em-service Chart](/deployment/helm/em-service) as of 2026-05-14). Major em-service version bumps trigger a fixture refresh; `npm run check-snippets` (CI) fails if the inline snippets drift from the canonical fixtures. The simplest viable solution: one HTTP service, one image, JWT validation, env-injected secrets. Maps to the [Quickstart](/guides/solution-dev/quickstart) shape with auth and secrets wired in. ### File tree ```text theme={null} / ├── packages/api/ │ ├── pyproject.toml │ └── src/api/ │ ├── __init__.py │ ├── main.py │ └── auth.py ├── Dockerfile └── charts// ├── Chart.yaml ├── values.yaml └── values.dev.yaml ``` ### Files ```toml packages/api/pyproject.toml theme={null} [project] name = "-api" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.110", "uvicorn[standard]>=0.30", "python-jose[cryptography]>=3.3", "httpx>=0.27", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/api"] ``` ```python packages/api/src/api/main.py theme={null} from fastapi import Depends, FastAPI from typing import Annotated from .auth import current_user, project_id app = FastAPI(title="") @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/echo") async def echo( msg: str, user: Annotated[dict, Depends(current_user)], proj: Annotated[str, Depends(project_id)], ) -> dict[str, str]: return {"echo": msg, "user_sub": user["sub"], "project_id": proj} ``` ```python packages/api/src/api/auth.py theme={null} import os import time from typing import Annotated import httpx from fastapi import Header, HTTPException, status from jose import jwt, JWTError KEYCLOAK_ISSUER_URL = os.environ["KEYCLOAK_ISSUER_URL"] KEYCLOAK_AUDIENCE = os.environ["KEYCLOAK_AUDIENCE"] # JWKS TTL — Keycloak rotates signing keys (default ~12h) _JWKS_TTL_SECONDS = 600 _jwks_cache: dict | None = None _jwks_cached_at: float = 0.0 def _jwks() -> dict: global _jwks_cache, _jwks_cached_at now = time.monotonic() if _jwks_cache is None or now - _jwks_cached_at > _JWKS_TTL_SECONDS: url = f"{KEYCLOAK_ISSUER_URL.rstrip('/')}/protocol/openid-connect/certs" _jwks_cache = httpx.get(url, timeout=5.0).raise_for_status().json() _jwks_cached_at = now return _jwks_cache def current_user(authorization: Annotated[str | None, Header()] = None) -> dict: if not authorization or not authorization.lower().startswith("bearer "): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token") try: return jwt.decode( authorization.split(" ", 1)[1], _jwks(), algorithms=["RS256"], audience=KEYCLOAK_AUDIENCE, issuer=KEYCLOAK_ISSUER_URL, ) except JWTError as e: raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}") def project_id(x_project_id: Annotated[str | None, Header()] = None) -> str: if not x_project_id: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing X-Project-ID header") return x_project_id ``` ```dockerfile Dockerfile theme={null} FROM python:3.12-slim AS build WORKDIR /app RUN pip install --no-cache-dir uv COPY packages/api/pyproject.toml packages/api/pyproject.toml COPY packages/api/src packages/api/src RUN uv pip install --system --no-cache-dir ./packages/api FROM python:3.12-slim COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn EXPOSE 8000 CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` ```yaml charts//Chart.yaml theme={null} apiVersion: v2 name: description: A minimal CRAFT solution type: application version: 0.1.0 appVersion: "0.1.0" dependencies: - name: em-service alias: api version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts ``` ```yaml charts//values.yaml theme={null} api: fullnameOverride: -api image: repository: /-api tag: 0.1.0 service: type: ClusterIP port: 8000 env: LOG_LEVEL: "INFO" KEYCLOAK_ISSUER_URL: "https://keycloak.example.com/realms/acme" KEYCLOAK_AUDIENCE: "-api" readinessProbe: { httpGet: { path: /healthz, port: 8000 } } livenessProbe: { httpGet: { path: /healthz, port: 8000 } } ``` ```yaml charts//values.dev.yaml theme={null} api: image: { tag: dev } replicaCount: 1 env: LOG_LEVEL: "DEBUG" KEYCLOAK_ISSUER_URL: "http://localhost:8080/realms/dev" ``` ### Mirrored fixtures Canonical copies of the eight files above live at `scripts/tests/fixtures/solution-dev/tier1/`. `npm run check-snippets` compares the inline snippets to the fixtures and fails if they drift. Adds a worker component, a Postgres database, and Alembic migrations. Mirrors the [Data Governance (em-data-readiness)](/data-governance/overview) shape. Below is the **diff** from Tier 1 — apply on top of Tier 1 to get Tier 2. ### File tree (additions) ```text theme={null} / ├── packages/ │ ├── api/ (unchanged from Tier 1) │ ├── worker/ │ │ ├── pyproject.toml │ │ └── src/worker/ │ │ ├── __init__.py │ │ └── main.py │ └── common/ │ ├── pyproject.toml │ └── src/common/ │ ├── __init__.py │ ├── db.py │ └── models.py ├── alembic.ini ├── alembic/ │ ├── env.py │ └── versions/ │ └── 0001_initial.py └── charts// ├── Chart.yaml (add em-service alias for worker) └── values.yaml (add worker block + DATABASE_URL secret) ``` ### Chart.yaml diff ```yaml charts//Chart.yaml theme={null} apiVersion: v2 name: type: application version: 0.1.0 dependencies: - name: em-service alias: api version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts # NEW for Tier 2 - name: em-service alias: worker version: 0.0.15 repository: oci://ghcr.io/emergenceai/em-charts ``` ### values.yaml diff ```yaml charts//values.yaml theme={null} api: # ... existing Tier 1 api block ... envVars: # NEW: DB connection string from secret - name: DATABASE_URL valueFrom: secretKeyRef: name: -secrets key: database-url # NEW for Tier 2 worker: image: repository: /-worker tag: 0.1.0 service: enabled: false env: LOG_LEVEL: "INFO" envVars: - name: DATABASE_URL valueFrom: { secretKeyRef: { name: -secrets, key: database-url } } ``` ### Worker entry point ```python packages/worker/src/worker/main.py theme={null} import asyncio import logging import os from common.db import session_factory logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) log = logging.getLogger("worker") async def process_one() -> None: async with session_factory() as session: # ... pull a job, run it, commit ... log.info("processed job") async def main() -> None: while True: try: await process_one() except Exception: log.exception("worker iteration failed") await asyncio.sleep(1.0) if __name__ == "__main__": asyncio.run(main()) ``` ### Common DB module ```python packages/common/src/common/db.py theme={null} import os from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine engine = create_async_engine(os.environ["DATABASE_URL"], echo=False, future=True) session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) ``` Reference implementation: see [Data Governance overview](/data-governance/overview) — the `em-data-readiness` repo follows this exact pattern. Adds an A2A agent component with LiteLLM and (optionally) a Prefect flow. Mirrors the [Data Insights (em-talk2data)](/data-insights/overview) shape. Below is the **diff** from Tier 2. ### File tree (additions) ```text theme={null} / ├── packages/ │ ├── api/ (unchanged) │ ├── worker/ (unchanged) │ ├── common/ (unchanged) │ ├── agent/ │ │ ├── pyproject.toml │ │ └── src/agent/ │ │ ├── __init__.py │ │ ├── card.py # Agent Card author │ │ ├── llm.py # LiteLLM client wrapper │ │ └── server.py # A2A protocol server │ └── flows/ # optional Prefect flows │ ├── pyproject.toml │ └── src/flows/ │ ├── __init__.py │ └── pipeline.py └── charts// ├── Chart.yaml (add em-service alias for agent + optional flows) └── values.yaml (add agent block with LLM env) ``` ### Chart.yaml diff ```yaml charts//Chart.yaml theme={null} dependencies: - { name: em-service, alias: api, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts } - { name: em-service, alias: worker, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts } # NEW for Tier 3 - { name: em-service, alias: agent, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts } ``` ### values.yaml additions for the agent ```yaml charts//values.yaml theme={null} agent: image: repository: /-agent tag: 0.1.0 service: type: ClusterIP port: 8001 env: LOG_LEVEL: "INFO" AGENT_NAME: "-agent" LLM_GATEWAY_URL: "https://litellm.example.com/v1" LLM_DEFAULT_MODEL: "gemini-3.5-flash" LANGFUSE_HOST: "https://langfuse.example.com" envVars: - name: LLM_GATEWAY_API_KEY valueFrom: { secretKeyRef: { name: -secrets, key: llm-gateway-api-key } } - name: LANGFUSE_PUBLIC_KEY valueFrom: { secretKeyRef: { name: -secrets, key: langfuse-public-key } } - name: LANGFUSE_SECRET_KEY valueFrom: { secretKeyRef: { name: -secrets, key: langfuse-secret-key } } readinessProbe: { httpGet: { path: /.well-known/agent-card.json, port: 8001 } } livenessProbe: { httpGet: { path: /.well-known/agent-card.json, port: 8001 } } ``` ### Agent Card ```python packages/agent/src/agent/card.py theme={null} AGENT_CARD = { "name": "-agent", "description": "", "version": "0.1.0", "url": "https://-agent.example.com", "protocolVersion": "0.3", "capabilities": {"streaming": True, "pushNotifications": False}, "skills": [ { "id": "answer", "name": "Answer questions", "description": "Answers questions using the project's data", "tags": ["qa"], "examples": ["What's the trend in last quarter's sales?"], } ], "securitySchemes": {"bearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}}, "security": [{"bearer": []}], } ``` ### Register the agent Follow [Register Your First Agent](/guides/first-agent) — the agent must be registered with Assets after first deployment so other services and the UI can discover it. ### Reference implementation `em-talk2data` is the canonical reference — see [Data Insights overview](/data-insights/overview) for the full architecture (gateway service + insights agent + text2sql agent + LiteLLM via the same gateway pattern shown above). ## Snippet drift check The Tier 1 fixture files at `scripts/tests/fixtures/solution-dev/tier1/` are the source of truth for the inline snippets above. Run `npm run check-snippets` to verify they match — CI runs this check on every PR. If you change Tier 1 inline content, update the fixtures (or vice versa), then re-run `npm run check-snippets`. ## Next steps Walk through Tier 1 step by step. Multi-component packaging and per-environment overlays. Required for Tier 3 agents to be discoverable. The Tier 3 reference solution in production. # Troubleshooting Source: https://docs.emergence.ai/guides/solution-dev/troubleshooting Recognise and fix the most common errors encountered when building solutions on CRAFT — auth, secrets, image pulls, helm conflicts, LLM failures. # Troubleshooting Symptom-first reference. Find the closest match below and follow the diagnostic steps. If your symptom isn't here, file an issue with label `solution-dev-guide` and the symptom will land in the next revision. ## 401 / 403 from your service **Diagnose:** ```bash theme={null} curl -v -H "Authorization: Bearer $TOKEN" -H "X-Project-ID: $PROJECT_ID" \ http://localhost:8000/echo?msg=hi 2>&1 | head -30 ``` **Common causes:** * Token missing → request didn't include `Authorization` header * Token expired → check `exp` claim with `jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$TOKEN"` * Wrong audience → your service's `KEYCLOAK_AUDIENCE` env doesn't match the `aud` claim * Wrong issuer → `KEYCLOAK_ISSUER_URL` doesn't match the realm in the `iss` claim * JWKS cache stale after Keycloak key rotation → restart your pod (or implement a TTL cache) **Diagnose:** ```bash theme={null} # Confirm Governance sees the user with the expected roles curl -s -H "Authorization: Bearer $TOKEN" -H "X-Project-ID: $PROJECT_ID" \ http://em-runtime-governance.em-runtime:8000/governance/whoami | jq . ``` **Common causes:** * User lacks the OpenFGA tuple for the action — see [RBAC Configuration](/guides/rbac-configuration) * `X-Project-ID` points to a project the user has no access to * You're calling the permission check with the wrong `resource_uri` shape Every authenticated route on the platform expects `X-Project-ID`. Add it to your test client / SDK calls. In FastAPI, use the `project_id` dependency from [Authenticate Users](/guides/solution-dev/authenticate-users). ## Pod crashes or "secret not found" **Diagnose:** ```bash theme={null} kubectl -n em- describe pod | grep -A2 "Failed" kubectl -n em- get secret -secrets # exists? kubectl -n em- get externalsecret # if ESO; check sync status ``` **Common causes:** * ESO/Infisical hasn't synced yet → wait one sync interval, or trigger manually * Upstream secret doesn't exist → create it in GCP Secret Manager / Infisical / Vault * SecretStore points to wrong project/path → check ClusterSecretStore config * K8s ServiceAccount missing Workload Identity annotation (GCP) → see [Secrets deployment](/deployment/infrastructure/secrets) **Diagnose:** ```bash theme={null} kubectl -n em- exec -- printenv DATABASE_URL kubectl -n em- get secret -secrets -o jsonpath='{.data.database-url}' | base64 -d ``` **Common causes:** * Key name mismatch between `secretKeyRef.key` and the K8s Secret data key * The `env:` map in `values.yaml` overrides `envVars` with the same name (the [em-service env model](/deployment/helm/em-service#environment-variable-model) deduplicates with `env` winning) → rename one of them * Secret value is literally empty in upstream (you set it to `""` somewhere) Stakater Reloader needs an annotation to know which Secret to watch. For env-injected secrets via `envFrom`, `em-service` adds it by default. For custom volume-mounted secrets, you must add `secret.reloader.stakater.com/reload: ""` to `podAnnotations` — see [Manage Secrets › Rotation](/guides/solution-dev/manage-secrets#rotation). ## Image pull errors **Diagnose:** ```bash theme={null} kubectl -n em- describe pod | grep -A3 "Failed to pull" ``` **Common causes:** * `image.repository` or `image.tag` is wrong * Registry is private and `imagePullSecrets` not configured * Pull-secret expired (registry tokens often have short lifetimes) * Cluster cannot reach the registry (network policy / air-gapped environment) * Wrong CPU architecture (e.g., arm64 image on amd64 cluster) — check with `docker manifest inspect` Some registries (GHCR, ACR) enforce IP allowlists. Check the registry's audit log for the cluster's egress IP and add it to the allowlist. For GHCR specifically, the personal access token used must have `read:packages` scope. ## Helm upgrade fails Kubernetes Deployments have an immutable `.spec.selector`. Once your release exists, you cannot change `app.kubernetes.io/name`, alias names, or labels that affect the selector. **Workaround**: `helm uninstall` and `helm install` (loses pod identity for \~1 cycle) — or re-create the namespace. If you rename an `em-service` alias from `api` to `webapp`, every reference in `values.yaml` (and any `--set`) must update too. The platform won't warn — it'll just deploy with default values. Check rendered output: ```bash theme={null} helm template ./charts/ -f values.yaml | grep -A5 "kind: Deployment" ``` Helm dependency cache is per-chart. After bumping the `em-service` version in `Chart.yaml`, run `helm dependency update ./charts/` before `helm upgrade`. Stale `Chart.lock` causes the old version to be deployed silently. If a previous release left orphan resources (PVCs, PDBs), `helm install` fails with "exists and cannot be imported into the current release." Delete the orphans manually or use `helm install --replace`. ## CRD not found A CRD your chart references isn't installed. Most commonly: * Gateway API (`gateway.networking.k8s.io/v1`) → install via `kubectl apply -k 'github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.1.0'` * ExternalSecret (`external-secrets.io/v1beta1`) → install ESO via `helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace` * The platform's own CRDs (if you depend on `em-core`) → install em-core first; see [em-core Chart](/deployment/helm/em-core) Recent `em-service` chart versions target Gateway API `v1` (`gateway.networking.k8s.io/v1`), while some older clusters only have `v1beta1`. Either upgrade the cluster's Gateway API CRDs (see above), or pin `em-service` to a chart version that still targets `v1beta1`. Check the [em-service Chart](/deployment/helm/em-service) reference for the Gateway API version each release uses. ## LLM gateway errors Per-project rate limit hit. Back off (`tenacity` with `wait_random_exponential`), then surface a friendly message. **Don't** retry forever. Upstream provider rejected or gateway lost a route. Configure fallbacks in the call: ```python theme={null} # Fallback to a different provider first (claude-opus-4-8 via Vertex) # rather than to a preview model. The customtools variant is specialised # for tool-heavy agents and isn't appropriate as a general fallback. await litellm.acompletion( model="gemini-3.5-flash", messages=[...], fallbacks=["claude-opus-4-8", "vertex_ai/gemini-3.1-pro-preview"], ) ``` `LLM_GATEWAY_API_KEY` rotated but pod hasn't restarted. Check Reloader annotation; manually `kubectl rollout restart` if needed. A code path called `litellm.acompletion(...)` without `metadata={"project_id": ..., "solution": ...}`. Grep for the call sites and confirm every one passes metadata. The HTTP client's read timeout is shorter than the model's first-token latency. Bump it: `httpx.AsyncClient(timeout=httpx.Timeout(connect=5.0, read=120.0, write=5.0, pool=5.0))`. ## Postgres errors Pod-to-Postgres DNS using the wrong service name. Use the K8s service DNS: ```text theme={null} postgresql+asyncpg://:@-postgresql.em-.svc.cluster.local:5432/ ``` Not `localhost`, not the pod IP. Add a `waitForPostgres` init container, or use a startup probe that retries until the DB accepts connections. Run migrations as a Helm hook (`helm.sh/hook: pre-install,pre-upgrade`) so only one pod ever runs them at a time. ## Local dev gotchas Use `host.docker.internal` (macOS/Windows) or your host LAN IP (Linux). Or run dependencies inside Kind via `helm install`. Set `OBSTORE_PATH_STYLE=true`. MinIO requires path-style addressing. Pass `--reload-dir packages/api/src` if your code lives outside the cwd. Editor "atomic write" may not trigger watchdog → switch the editor's save mode. `docker compose ps` shows `unhealthy`? Check `docker compose logs `. The compose file in [Local Development](/guides/solution-dev/local-development) sets healthchecks; failures usually mean port conflicts or insufficient memory. ## Still stuck? Report via the 👎 thumbs at the bottom of the affected page — include: 1. The exact symptom (with command + output) 2. What you've already tried 3. Your solution's name and the cluster context # Use Shared Storage Source: https://docs.emergence.ai/guides/solution-dev/use-shared-storage Read and write objects through the platform's infrastructure-agnostic obstore client — works against S3, GCS, Azure Blob, and MinIO. # Use Shared Storage The platform exposes object storage through `obstore`, a thin abstraction that lets the same code run against AWS S3, GCS, Azure Blob, and local MinIO. Solutions use it for artifacts, intermediate data, files served to the UI, and anything else that doesn't belong in the relational database. For the platform-side abstraction model, see [Platform Overview](/platform/overview). For data-source connections (databases + storage credentials registered by customers), see [Platform › Data Connections](/platform/data-connections). ## Why obstore Three things you get for free: 1. **One code path, all clouds.** No `if AWS: ... elif GCP: ...` in your service. 2. **Credential rotation handled by the platform.** Connections fetch fresh credentials per request via the secrets pipeline. 3. **Per-tenant path isolation enforced by convention.** Every operation is scoped by `org_id` + `project_id` (passed via `X-Project-ID`). ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR subgraph SOL["Your solution"] CODE["one code path
obs.put / obs.get"] PREFIX["path prefix:
<org_id>/<project_id>
/<solution>/..."] CODE --> PREFIX end OBS["obstore client
(env-configured backend)"] PREFIX --> OBS OBS --> S3[("AWS S3")] OBS --> GCS[("GCS")] OBS --> AZ[("Azure Blob")] OBS --> MIN[("MinIO
(local dev)")] classDef sol fill:#56B4E9,stroke:#555555,color:#000 classDef act fill:#E69F00,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef store fill:#009E73,stroke:#555555,color:#000 class CODE,PREFIX sol class OBS infra class S3,GCS,AZ,MIN store ``` **Read left-to-right**: your code calls `obs.put` / `obs.get` with a path scoped by `org_id`/`project_id`/`solution`; the obstore client picks the actual backend from env config; the same code runs against any of S3/GCS/Azure/MinIO. ## Path conventions Stick to this prefix layout for everything you write: ```text theme={null} ///// ``` Example: ```text theme={null} /acme-corp/proj-42/talk2data/conversations/abc123/messages.jsonl ``` Predictable paths make backup, garbage collection, and audit trivial. ## Provision a connection Customers register a [Data Connection](/platform/data-connections) (type `s3`, `gcs`, or `minio`) via Assets. Your solution looks it up by name when it needs to read or write. The customer registers the connection in the UI or via the Assets API, then grants your solution permission. Your code: ```python theme={null} from em_runtime_assets_sdk import AuthenticatedClient from em_runtime_assets_sdk.api.connections import get_connection async def get_storage_creds(token: str, project_id: str, conn_name: str) -> dict: client = AuthenticatedClient(base_url="http://em-runtime-assets.em-runtime:8000", token=token) conn = await get_connection.asyncio(client=client, x_project_id=project_id, name=conn_name) # conn.config holds bucket/region/endpoint; credentials fetched server-side via Secrets API return { "endpoint": conn.config.endpoint, "bucket": conn.config.bucket, "region": conn.config.region, "access_key": conn.credentials.access_key, "secret_key": conn.credentials.secret_key, } ``` For solution-owned scratch storage (logs, intermediate Prefect outputs), put bucket details directly in your chart and inject creds via secrets: ```yaml charts//values.yaml theme={null} api: env: OBSTORE_ENDPOINT: "https://s3.us-east-1.amazonaws.com" OBSTORE_BUCKET: "-artifacts" OBSTORE_REGION: "us-east-1" envVars: - name: OBSTORE_ACCESS_KEY valueFrom: { secretKeyRef: { name: -secrets, key: storage-access-key } } - name: OBSTORE_SECRET_KEY valueFrom: { secretKeyRef: { name: -secrets, key: storage-secret-key } } ``` ## Code patterns The `obstore` Python package gives you a single API across providers. Install with `uv add obstore`. ### Initialize a client from env ```python packages/api/src/api/storage.py theme={null} import os from obstore.store import S3Store, GCSStore, AzureStore, LocalStore def make_store(): backend = os.environ.get("OBSTORE_BACKEND", "s3") if backend == "s3": return S3Store( bucket=os.environ["OBSTORE_BUCKET"], endpoint=os.environ.get("OBSTORE_ENDPOINT"), # MinIO sets this; AWS doesn't access_key_id=os.environ["OBSTORE_ACCESS_KEY"], secret_access_key=os.environ["OBSTORE_SECRET_KEY"], region=os.environ.get("OBSTORE_REGION", "us-east-1"), virtual_hosted_style_request=os.environ.get("OBSTORE_PATH_STYLE", "false") != "true", ) if backend == "gcs": return GCSStore(bucket=os.environ["OBSTORE_BUCKET"]) # Workload Identity if backend == "azure": return AzureStore(container=os.environ["OBSTORE_BUCKET"]) if backend == "local": return LocalStore(prefix=os.environ.get("OBSTORE_LOCAL_PREFIX", "/tmp/obstore")) raise ValueError(f"Unknown backend: {backend}") ``` ### Put / get / list / delete ```python theme={null} import obstore as obs store = make_store() # PUT (small) — bytes in, path key out await obs.put_async(store, "acme-corp/proj-42/talk2data/conversations/abc123.json", b'{"hi":"there"}') # GET (small) data = await obs.get_async(store, "acme-corp/proj-42/talk2data/conversations/abc123.json") print(data.bytes()) # b'{"hi":"there"}' # LIST (paginated; iterate the async iterator) async for batch in obs.list_async(store, prefix="acme-corp/proj-42/"): for entry in batch: print(entry.path, entry.size) # DELETE await obs.delete_async(store, "acme-corp/proj-42/talk2data/conversations/abc123.json") ``` ### Stream a large object (avoid loading into memory) ```python theme={null} async def stream_to_disk(store, key: str, dest: str) -> None: response = await obs.get_async(store, key) with open(dest, "wb") as f: async for chunk in response.stream(min_chunk_size=8 * 1024 * 1024): f.write(chunk) ``` For multi-GB objects, prefer streaming over `bytes()`. ## Per-tenant isolation `X-Project-ID` is mandatory on every storage operation. Build the prefix from request context: ```python theme={null} from fastapi import Depends, HTTPException from typing import Annotated from .auth import current_user, project_id def storage_prefix( user: Annotated[dict, Depends(current_user)], proj: Annotated[str, Depends(project_id)], solution: str = "hello-solution", ) -> str: org = user.get("org_id") or org_id_from_issuer(user["iss"]) if not org or not proj: raise HTTPException(400, "Missing org_id or project_id") return f"{org}/{proj}/{solution}" ``` Now every read/write is scoped — there is no path a request can construct that crosses tenants. See [Platform › Multi-tenancy](/platform/multi-tenancy) for the org/project model. ## Local development with MinIO The [Local Development](/guides/solution-dev/local-development#mode-1-docker-compose-with-hot-reload) page shows the docker-compose snippet. Pre-create the bucket once: ```bash theme={null} docker run --rm --network host minio/mc \ sh -c "mc alias set local http://localhost:9000 minioadmin minioadmin && \ mc mb -p local/-artifacts" ``` Then set `OBSTORE_BACKEND=s3`, `OBSTORE_ENDPOINT=http://localhost:9000`, `OBSTORE_PATH_STYLE=true`. The same code that talks to AWS S3 in prod talks to MinIO in dev. ## Common errors Set `OBSTORE_PATH_STYLE=true`. MinIO doesn't support virtual-hosted-style addressing without DNS configuration. Check that the bucket policy / Workload Identity SA grants the required action. For GCS Workload Identity, verify the K8s ServiceAccount is annotated with `iam.gke.io/gcp-service-account=` and the GSA has `roles/storage.objectAdmin` on the bucket. AWS S3 SDKs require a region; set `OBSTORE_REGION` even if you're targeting a non-AWS endpoint via `OBSTORE_ENDPOINT`. Your `storage_prefix` helper isn't being used everywhere, OR your route accepts a user-controlled key without prefixing. Audit by grepping for `obs.get_async\|obs.put_async\|obs.delete_async` in your service and confirming every call site composes the path through `storage_prefix(...)`. ## Verification ```bash theme={null} # Round-trip test (local with MinIO) PUT_RESULT=$(curl -s -X PUT \ -H "Authorization: Bearer $TOKEN" -H "X-Project-ID: $PROJECT_ID" \ http://localhost:8000/storage/test.txt --data-binary "hello") GET_RESULT=$(curl -s \ -H "Authorization: Bearer $TOKEN" -H "X-Project-ID: $PROJECT_ID" \ http://localhost:8000/storage/test.txt) [ "$GET_RESULT" = "hello" ] && echo "round-trip OK" || echo "FAIL" ``` ## Next steps How customers register storage and DB connections. Org/project isolation — what enforces it where. Storage credentials follow the same secret-injection pattern. More storage error scenarios. # SSO Integration Source: https://docs.emergence.ai/guides/sso-integration How to configure Single Sign-On with Keycloak using OIDC or SAML for enterprise identity providers like Entra ID, Okta, and Google Workspace. # Configure SSO with Keycloak CRAFT uses **Keycloak** as its identity provider, supporting Single Sign-On (SSO) via both **OIDC** and **SAML 2.0** protocols. This guide covers configuring SSO for enterprise identity providers including Microsoft Entra ID (Azure AD), Okta, Google Workspace, and PingFederate. ## Prerequisites Before you begin, ensure you have: * Administrative access to the Keycloak admin console * Administrative access to your enterprise identity provider (IdP) * The Keycloak realm ID for your organization (realm ID = organization ID) * Network connectivity between Keycloak and the IdP (HTTPS required) ## How SSO Works in CRAFT Each organization in the platform maps to a **Keycloak realm**. When SSO is configured: 1. Users navigate to the platform login page 2. Keycloak redirects to the enterprise IdP for authentication 3. The IdP authenticates the user and returns claims to Keycloak 4. Keycloak issues a JWT containing `org_id` (from the realm) and group memberships 5. The platform uses the JWT for authorization via OpenFGA Per-tenant SSO configuration means each organization can connect to a different identity provider. One organization might use Okta while another uses Entra ID. ## Configure OIDC SSO OIDC is the recommended protocol for most identity providers. 1. Navigate to **Azure Portal** > **Microsoft Entra ID** > **App registrations** > **New registration** 2. Set the **Redirect URI** to: `https:///realms//broker/entra-id/endpoint` 3. Note the **Application (client) ID** and **Directory (tenant) ID** 4. Under **Certificates & secrets**, create a new client secret and note the value 1. In the app registration, go to **Token configuration** > **Add groups claim** 2. Select **Security groups** and set the token type to **ID** 3. Ensure the groups claim format is set to **Group ID** 1. Log in to Keycloak Admin Console 2. Select the realm matching your organization ID 3. Navigate to **Identity Providers** > **Add provider** > **OpenID Connect v1.0** 4. Configure the following: | Field | Value | | ----------------- | --------------------------------------------------------------------- | | Alias | `entra-id` | | Authorization URL | `https://login.microsoftonline.com//oauth2/v2.0/authorize` | | Token URL | `https://login.microsoftonline.com//oauth2/v2.0/token` | | Client ID | Your Entra ID application client ID | | Client Secret | Your Entra ID client secret | | Default Scopes | `openid email profile` | In the IdP configuration, add an **Attribute Importer** mapper: * **Mapper Type:** Attribute Importer * **Claim:** `groups` * **User Attribute Name:** `groups` 1. Navigate to **Okta Admin** > **Applications** > **Create App Integration** 2. Select **OIDC - OpenID Connect** and **Web Application** 3. Set the **Sign-in redirect URI** to: `https:///realms//broker/okta/endpoint` 4. Note the **Client ID** and **Client secret** 1. In the application settings, go to **Sign On** > **OpenID Connect ID Token** 2. Set **Groups claim type** to **Filter** 3. Set **Groups claim filter** to match the groups you want to sync 1. Select the realm matching your organization ID 2. Navigate to **Identity Providers** > **Add provider** > **OpenID Connect v1.0** 3. Configure: | Field | Value | | ----------------- | ------------------------------------------- | | Alias | `okta` | | Authorization URL | `https:///oauth2/v1/authorize` | | Token URL | `https:///oauth2/v1/token` | | Client ID | Your Okta client ID | | Client Secret | Your Okta client secret | | Default Scopes | `openid email profile groups` | 1. Navigate to **Google Cloud Console** > **APIs & Services** > **Credentials** 2. Create an **OAuth 2.0 Client ID** of type **Web application** 3. Add the redirect URI: `https:///realms//broker/google/endpoint` 4. Note the **Client ID** and **Client secret** 1. Select the realm matching your organization ID 2. Navigate to **Identity Providers** > **Add provider** > **Google** 3. Enter the Client ID and Client Secret 4. Set **Default Scopes** to `openid email profile` ## Configure SAML 2.0 SSO For identity providers that prefer SAML, Keycloak supports SAML 2.0 as well. Download the SAML metadata from your Keycloak realm: ``` https:///realms//protocol/saml/descriptor ``` Upload this XML to your IdP as the service provider metadata. In your SAML IdP, configure the following: | Field | Value | | -------------- | ----------------------------------------------------------------- | | Entity ID | `https:///realms/` | | ACS URL | `https:///realms//broker//endpoint` | | Name ID format | `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` | 1. Navigate to **Identity Providers** > **Add provider** > **SAML v2.0** 2. Import the IdP's SAML metadata XML or manually configure: * **Single Sign-On Service URL** * **Single Logout Service URL** (optional) * **Signing certificate** 3. Set **NameID Policy Format** to `email` ## Just-In-Time User Provisioning Keycloak supports **JIT provisioning** by default. When a user authenticates via SSO for the first time: 1. Keycloak creates a local user account in the realm 2. User attributes (email, name) are synced from the IdP 3. Group memberships are mapped from IdP claims to Keycloak groups 4. OpenFGA relationships are established based on group-to-role mappings JIT provisioning eliminates the need to pre-create user accounts. Users are provisioned automatically on their first SSO login. ## Enforce SSO (Disable Password Login) To require SSO for all users in an organization: 1. In the Keycloak admin console, select the organization's realm 2. Navigate to **Authentication** > **Flows** 3. Edit the **Browser** flow to remove or disable the **Username Password Form** 4. Ensure the **Identity Provider Redirector** is enabled and set to your SSO provider Keycloak client credentials for service accounts remain available even when password login is disabled. This ensures CI/CD pipelines and machine-to-machine integrations continue to function. ## Session Management Configure session behavior for your organization: | Setting | Location | Description | | ---------------- | ------------------------- | ------------------------------------------------------- | | SSO Session Idle | Realm Settings > Sessions | Idle timeout before re-authentication (default: 30 min) | | SSO Session Max | Realm Settings > Sessions | Maximum session duration (default: 10 hours) | | Remember Me | Realm Settings > Login | Allow users to extend their session | ## Group-to-Role Mapping Map IdP groups to OpenFGA roles for authorization: | IdP Group | Keycloak Group | OpenFGA Role | | ----------------- | -------------- | ------------ | | `Platform-Admins` | `admins` | `admin` | | `Developers` | `developers` | `developer` | | `Data-Analysts` | `members` | `member` | | `Viewers` | `viewers` | `viewer` | The mapping pipeline flows: **IdP groups** -> **Keycloak groups** -> **OpenFGA relations**. See [RBAC Configuration](/guides/rbac-configuration) for setting up the OpenFGA side. ## Troubleshooting Verify the redirect URI in your IdP matches the Keycloak broker endpoint exactly, including the protocol (HTTPS) and realm ID. Check for trailing slashes. Ensure group claims are configured in the IdP and a group mapper is set up in the Keycloak identity provider configuration. Verify the claim name matches between the IdP and the Keycloak mapper. Groups must be mapped from the IdP through Keycloak to OpenFGA. If the group-to-role mapping pipeline is not configured, users will authenticate but have no permissions. See the Group-to-Role Mapping section above. ## Next Steps Configure OpenFGA roles and permissions to complete the authorization setup. Learn about OIDC flows, token management, and machine-to-machine authentication. Automate user provisioning with SCIM 2.0 for SOC 2 compliance. Understand how SSO integrates with the multi-tenant architecture. # Agent Registry Source: https://docs.emergence.ai/platform/agents Unified registry for discovering, managing, and monitoring A2A agents, MCP servers, and agentskills.io skills at enterprise scale. # Agent Registry The Agent Registry provides a discovery layer for AI agents and tools. The core A2A Agent Registry is available now with full CRUD, fine-grained authorization, and event streaming. The registry fills the explicit gap in the A2A specification, which acknowledges the need for curated registries but does not prescribe a standard API for them. The A2A specification states: *"The current A2A specification does not prescribe a standard API for curated registries"* and *"The A2A community explores standardizing registry interactions or advanced discovery protocols."* The Agent Registry is designed to fill this gap with enterprise-grade capabilities. ## The Discovery Problem The A2A protocol v1.0 defines three agent discovery strategies, two of which are limited: | Strategy | Limitation | | --------------------------------------------------- | ------------------------------------------------- | | **Well-Known URI** (`/.well-known/agent-card.json`) | Requires knowing the agent's domain upfront | | **Curated Registries** | Acknowledged as needed but explicitly unspecified | | **Direct Configuration** | Hardcoded and inflexible | Meanwhile, the MCP ecosystem has its own public registry, and agentskills.io provides a portable skill format. These three ecosystems are complementary but fragmented -- no unified discovery layer exists. ## Key Capabilities RESTful CRUD for Agent Cards with full schema validation, optimistic concurrency control, and fine-grained authorization. Agents can register, update, and manage their A2A-compliant Agent Cards through the registry API. Auto-detects A2A protocol version (0.3 vs 1.0) from card structure. SHA-256 card hash for duplicate detection. **API surface:** | Method | Endpoint | Description | | ------ | ------------------------ | ------------------------------------------------ | | POST | `/agents` | Register agent (201 + Location + ETag) | | GET | `/agents` | List with pagination, status/tag/version filters | | GET | `/agents/{resource_uri}` | Get by URI (ETag) | | PUT | `/agents/{resource_uri}` | Update card with If-Match version guard | | DELETE | `/agents/{resource_uri}` | Permanent delete, cleans up authorization tuples | | POST | `/agents/validate` | Dry-run card validation (no DB write) | Periodic multi-level health probes ensure agents are reachable and functioning: | Level | Check | Purpose | | ------ | --------------------- | ---------------------------------------------------- | | **L1** | Endpoint reachability | Is the agent's URL responding? | | **L2** | Card freshness | Has the Agent Card been updated recently? | | **L3** | Protocol probe | Does the agent respond to A2A/MCP protocol requests? | All probes include SSRF protection to prevent internal network scanning. Track agents through their full lifecycle with administrative status and lifecycle stages: **Administrative Status:** PENDING, ACTIVE, SUSPENDED, DEREGISTERED **Lifecycle Stage:** EXPERIMENTAL, STABLE, DEPRECATED, RETIRED Immutable version snapshots are created on every update to an agent's registration. The registry supports version diffing and rollback to any previous version. Enterprise-grade controls including: * Approval workflows for new agent registrations * Registration policies and change windows * Audit logging of all operations * Compliance reporting * Tenant-scoped administration with optional cross-tenant federation ## Architecture ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% stateDiagram-v2 [*] --> PENDING : POST /agents/register PENDING --> ACTIVE : L1+L2+L3 health probes pass ACTIVE --> SUSPENDED : admin suspend or health failure SUSPENDED --> ACTIVE : admin reactivate ACTIVE --> DEPRECATED : lifecycle transition DEPRECATED --> RETIRED : end-of-life SUSPENDED --> DEREGISTERED : admin delete ACTIVE --> DEREGISTERED : admin delete RETIRED --> DEREGISTERED : admin delete DEREGISTERED --> [*] ``` The Agent Registry follows a stateless API architecture backed by proven platform infrastructure, OIDC authentication, fine-grained authorization, event streaming, and relational storage. ## Artifact Preview Agents produce artifacts (query results, generated reports, intermediate data) that the platform can preview without forcing a full download. The Assets service exposes a preview endpoint that returns a structured snapshot of an artifact. **Defaults** (effective with em-runtime 5.3+): * Default row limit: **500 rows** (raised from 10) * Maximum byte budget: **512 KB** per response (raised from 64 KB) * Maximum row limit: **10,000 rows** (cap regardless of caller request) **Response metadata** (`PreviewMetadata`): * `size_bytes`: total file size for tabular, text, and raw artifacts (always present) * `truncated`: boolean flag set when either the row limit or the byte budget was reached The service applies a byte-level truncation backstop after row-level limits, so a response that cuts cleanly at a record boundary still respects `max_bytes`. Callers should always check `truncated` before assuming a preview reflects the full artifact. ## MCP Tool Integrations The em-runtime MCP server exposes tool sets that agents can invoke via the Model Context Protocol. These tools proxy authenticated calls to platform services using the caller's JWT and project context. ### Talk2Data Tools When `TALK2DATA_BASE_URL` is configured, the following tools are available: | Tool | Description | | --------------------------- | ---------------------------------------------------------------------------------------- | | `generate_sql_mcp` | Generate SQL from a natural-language question against a specified data source and schema | | `execute_query_mcp` | Execute a SQL query against a data connection and return results | | `resolve_term_mcp` | Resolve a business term to its canonical definition using the data catalog | | `generate_plotly_chart_mcp` | Generate a Plotly chart specification from query results | These tools forward requests to the Talk2Data Service using the caller's bearer token and `X-Project-ID` header. If `TALK2DATA_BASE_URL` is not set, the tools return a `talk2data_not_configured` error. See [Data Insights Overview](/data-insights/overview) for Talk2Data deployment. ### Catalog Tools The MCP server also exposes metadata and search tools against the Utils service (data catalog): * **`get_schema_mcp`** — Get schema metadata for a data connection entity by FQN * **`list_metadata_mcp`** — List metadata assets with filtering * **`search_metadata_mcp`** — Full-text search across the data catalog ## Webhook Events The registry emits events after successful registration operations. | Event Category | Examples | | ---------------- | ----------------------------------------------- | | **Registration** | Agent registered, updated, deregistered | | **Health** | Health status changed, probe failed | | **Lifecycle** | Stage transition (e.g., EXPERIMENTAL to STABLE) | | **Search** | New agent matches a saved search filter | ## Well-Known URI Crawling The registry can automatically discover agents from configured domains by crawling their `/.well-known/agent-card.json` endpoints: * Scheduled crawls with configurable intervals * Caching to minimize network traffic * **Drift detection** to identify when a remote Agent Card changes * SSRF protection for all outbound requests ## Multi-Tenancy The registry supports full multi-tenant isolation: * **Namespace isolation** -- each tenant's agents are invisible to other tenants * **Tenant-scoped administration** -- per-tenant admin roles and policies * **Cross-tenant federation** -- optional sharing of selected agents across tenants * **Cost attribution** -- per-tenant usage metering, quota enforcement, and chargeback ## Target Personas | Persona | Use Case | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Agent Developer** | Register, update, and manage A2A Agent Cards. Validate cards before publishing. Track health and manage versions. | | **MCP Server Developer** | Register MCP server metadata (tools, resources, prompts). Validate tool schemas. Publish to both this registry and the MCP public registry. | | **Consumer** | Search by skill tags, keywords, provider, capabilities, and protocol. Filter by health and verification status. | | **Platform Operator** | Deploy and scale the registry. Monitor health, latency, and throughput. Configure health check intervals. | | **Enterprise Admin** | Control registration policies. Configure approval workflows. Audit operations. Enforce security policies. | ## Success Metrics | Metric | Target | | ---------------------------- | ------------------------ | | API availability | 99.9% monthly | | Search p99 latency | \< 500ms | | Registration p99 latency | \< 1000ms | | Search index lag | \< 30 seconds | | Webhook first delivery | \< 60 seconds | | Registered entities capacity | 100K+ agents and servers | | Search throughput | 1K+ queries/second | ## Next Steps Configure data source connections used by agents for data access. Set up webhook notifications for agent lifecycle events. See how Data Insights uses A2A agents for conversational analytics. Understand authentication, authorization, and multi-tenant isolation. # Authentication Source: https://docs.emergence.ai/platform/authentication Keycloak multi-realm OIDC/PKCE authentication with JWT token validation, service accounts, and SSO for CRAFT. # Authentication CRAFT uses **Keycloak** as its identity provider, configured in a **multi-realm architecture** where each organization is a separate Keycloak realm. Authentication follows the **OpenID Connect (OIDC)** protocol with **PKCE** (Proof Key for Code Exchange) for browser-based clients. Keycloak is self-hosted -- the platform's identity layer runs on your infrastructure and does not depend on a cloud-native identity service. You can federate external enterprise identity providers (Entra ID, Okta, Google Workspace) through standard OIDC and SAML protocols. ## Multi-Realm Architecture Each organization maps to a dedicated Keycloak realm: ```text theme={null} Keycloak Server master realm -- Platform developers and service accounts acme-corp realm -- Acme Corporation users globex realm -- Globex Industries users initech realm -- Initech users ``` This provides complete identity isolation: * Users in one realm cannot see or access users in another * Each realm has independent password policies, session settings, and identity providers * Group memberships are realm-scoped and appear as JWT claims * SSO can be configured per-realm (e.g., SAML, LDAP, social login) ## Authentication Flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': { 'actorBkg': '#56B4E9', 'actorBorder': '#555555', 'actorTextColor': '#000000', 'actorLineColor': '#555555', 'signalColor': '#555555', 'signalTextColor': '#555555', 'labelBoxBkgColor': '#EEEEEE', 'labelBoxBorderColor': '#555555', 'labelTextColor': '#555555', 'loopTextColor': '#555555', 'noteBkgColor': '#F0E442', 'noteBorderColor': '#555555', 'noteTextColor': '#000000', 'activationBkgColor': '#E69F00', 'activationBorderColor': '#555555', 'fontFamily': 'sans-serif' }}}%% sequenceDiagram participant B as Browser participant KC as Keycloak Realm participant API as Platform API B->>B: Generate PKCE code_verifier + code_challenge B->>KC: Authorization request + code_challenge KC->>B: Redirect to login page B->>KC: User credentials KC-->>B: Authorization code (redirect) B->>KC: Token request + code_verifier KC-->>B: Access token (JWT) + refresh token + ID token B->>API: API request + Authorization: Bearer JWT API->>API: Validate JWT, extract org_id from realm API-->>B: Response ``` The client discovers the realm's OpenID configuration at: ```text theme={null} https://keycloak.example.com/realms/{org_id}/.well-known/openid-configuration ``` The browser initiates the OIDC authorization code flow with PKCE. A code verifier is generated client-side and the SHA-256 code challenge is sent with the authorization request. No client secret is required. The user authenticates at the Keycloak login page for their realm. Keycloak supports username/password, SSO via external IdPs (SAML, OIDC federation), LDAP, and social login. After successful login, Keycloak returns an authorization code. The client exchanges it (with the code verifier) for: * **Access token (JWT)** -- Used for API authorization * **Refresh token** -- Used to obtain new access tokens * **ID token** -- Contains user identity claims All API requests include the access token in the `Authorization: Bearer` header. The platform validates the token and extracts the organization context from the realm. ## JWT Token Structure The access token contains standard OIDC claims plus platform-specific claims: ```json theme={null} { "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "iss": "https://keycloak.example.com/realms/acme-corp", "aud": "account", "azp": "em-runtime-ui", "exp": 1743696000, "iat": 1743692400, "preferred_username": "jane.smith", "email": "jane.smith@acme-corp.com", "name": "Jane Smith", "groups": ["/org-admins", "/project-developers"], "realm_access": { "roles": ["default-roles-acme-corp"] } } ``` Key claims used by the platform: | Claim | Purpose | | ------------- | -------------------------------------------------------------------------- | | `sub` | User ID for OpenFGA permission checks | | `iss` | Realm extraction -- the path segment after `/realms/` becomes `org_id` | | `groups` | Keycloak group memberships, used for group-based OpenFGA permission checks | | `azp` | Client ID of the application that requested the token | | `exp` / `iat` | Token expiration and issuance timestamps | ## Token Validation The Governance service validates tokens using a multi-step process: 1. **Extract realm** -- The token's `iss` claim is decoded (without verification) to determine the Keycloak realm 2. **Fetch JWKS** -- Public keys are fetched from the realm's JWKS endpoint (cached per realm via `lru_cache`) 3. **Verify signature** -- The token signature is validated against the realm's public key, which cryptographically proves the token was issued by the platform's Keycloak instance 4. **Check expiration** -- Standard JWT expiration (`exp`) validation 5. **Verify issuer consistency** -- The issuer from the verified token must match the initial unverified extraction 6. **Extract claims** -- `org_id`, groups, roles, and user identity are extracted into the `Auth` object * **Issuer hostname is not validated separately** -- JWKS signature verification is strictly stronger. A token signed by the platform's Keycloak private key is accepted regardless of the hostname in the issuer URL. This allows tokens issued via any entrypoint (internal service URL, API gateway, external hostname) to be accepted. * **Realm-specific validation** -- Each realm has its own signing keys. A token from `realm-a` cannot be validated against `realm-b`'s JWKS. * **Group normalization** -- Leading slashes are stripped from group names (`/org-admins` becomes `org-admins`). ## Service Accounts Service accounts are used by background workers and automated processes that need to access the platform without user interaction. They are identified by a three-check detection. See [Service Accounts](/platform/service-accounts) for a complete guide including token management, creation steps, and code examples. Service accounts authenticate via the Keycloak `master` realm, not an organization realm. Their `org_id` is `null` by default. Service account client IDs must start with the `svc-` prefix (e.g., `svc-data-readiness`, `svc-talk2data`). This is a naming convention enforced by the detection logic. Service accounts must have the `serviceAccount` role in `realm_access.roles`. This provides defense in depth -- even if someone creates a client with a `svc-` prefix, it will not be treated as a service account without the role. Service accounts use additional headers to establish context: | Header | Purpose | Required | | ---------------- | -------------------------------------------------------------------------- | -------- | | `X-Org-Id` | Specifies the organization context (since master realm tokens have no org) | Yes | | `X-On-Behalf-Of` | Identifies the original user in audit logs when acting on their behalf | Optional | The `X-On-Behalf-Of` and `X-Org-Id` headers are only trusted when the caller passes all three service account checks. Regular user tokens cannot use these headers to impersonate other users or switch organizations. ## SSO Integration Each Keycloak realm can be configured with external identity providers for single sign-on: | Provider Type | Configuration | | ------------------- | --------------------------------------------------------- | | **SAML 2.0** | Enterprise IdP integration (Okta, Azure AD, PingFederate) | | **OIDC Federation** | Connect to another OIDC provider | | **LDAP/AD** | Sync users from Active Directory or LDAP | | **Social Login** | Google, GitHub, Microsoft (per-realm configuration) | Since each organization is a separate realm, SSO configuration is fully tenant-isolated. One organization can use SAML with Okta while another uses LDAP with Active Directory. ## Obtaining Tokens ```bash theme={null} # Get a JWT token for the default admin user make get-token # Customize organization or credentials ORGANIZATION_ID=acme-corp \ ADMIN_USERNAME=jane \ ADMIN_PASSWORD=secret \ make get-token ``` ```bash theme={null} # Get a Keycloak platform admin token (master realm) make get-platform-token ``` ```python theme={null} from keycloak import KeycloakOpenID keycloak = KeycloakOpenID( server_url="https://keycloak.example.com/", client_id="my-app", realm_name="acme-corp", ) # Resource Owner Password Grant (dev/testing only) token = keycloak.token("username", "password") access_token = token["access_token"] ``` ## Next Steps Learn how JWT claims are used for OpenFGA permission checks. Understand how organizations map to Keycloak realms. See the full tenant isolation architecture. Learn how the X-Project-ID header provides project context. # Authorization Source: https://docs.emergence.ai/platform/authorization OpenFGA Relationship-Based Access Control (ReBAC) with relationship tuples, computed permissions, hierarchical inheritance, and the require_permission pattern. # Authorization CRAFT uses **OpenFGA** for authorization, implementing **Relationship-Based Access Control (ReBAC)**. Unlike traditional RBAC where roles grant flat permission sets, ReBAC models permissions as relationships between users and resources, enabling fine-grained, hierarchical access control. OpenFGA is the open-source implementation of Google's Zanzibar authorization system. It evaluates permission checks in single-digit milliseconds, making it suitable for per-request authorization in API endpoints. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD U["User / Group"] O["Organization"] P["Project"] A["Agent"] DC["Data Connection"] MCP["MCP Server"] U -- "owner / admin / member" --> O O -- "owns" --> P U -- "developer / operator / viewer" --> P P -- "parent_of" --> A P -- "parent_of" --> DC P -- "parent_of" --> MCP style U fill:#56B4E9,stroke:#555555,color:#000 style O fill:#E69F00,stroke:#555555,color:#000 style P fill:#E69F00,stroke:#555555,color:#000 style A fill:#009E73,stroke:#555555,color:#000 style DC fill:#009E73,stroke:#555555,color:#000 style MCP fill:#009E73,stroke:#555555,color:#000 ``` ## Core Concepts A relationship tuple is the fundamental building block. It states: *"This user/group has this relation on this resource."* ```text theme={null} user:jane owner organization:acme-corp group:org-admins admin organization:acme-corp user:bob developer project:analytics-prod ``` Tuples are stored in OpenFGA and queried during permission checks. The authorization model defines types for every resource in the platform: | Type | Service | Description | | ----------------- | ---------- | ------------------------------------------- | | `organization` | Governance | Top-level tenant boundary | | `project` | Governance | Resource grouping within an organization | | `artifact` | Assets | Generated outputs (reports, visualizations) | | `file` | Assets | Uploaded files and documents | | `data_connection` | Assets | External data source connections | | `mcp_server` | Assets | Model Context Protocol servers | | `api_server` | Assets | API server registrations | | `model` | Assets | ML model registrations | Relations represent the roles a user or group can have on a resource: | Relation | Scope | Description | | ----------- | --------------------- | ---------------------------------------------- | | `owner` | All types | Full control including deletion | | `admin` | Organization, Project | Administrative access, user/project management | | `member` | Organization | Standard membership with read access | | `developer` | Project, Resources | Create and manage resources | | `operator` | Project, Resources | Execute and operate resources | | `viewer` | Project, Resources | Read-only access | Computed permissions are derived from relations using OR logic. For example, `can_read` is granted to anyone with `viewer`, `operator`, `developer`, `admin`, or `owner` relation: ```text theme={null} define can_read: viewer or operator or developer or admin or owner ``` The full set of computed permissions: | Permission | Description | | ---------------------- | --------------------------------------------------------------- | | `can_read` | View the resource | | `can_write` | Modify the resource | | `can_delete` | Remove the resource | | `can_execute` | Run/invoke the resource (agents, MCP servers, data connections) | | `can_create_resources` | Create child resources (project-level) | | `can_share` | Share with other users | | `can_read_secrets` | View associated secrets | | `can_manage_secrets` | Create/update/delete secrets | | `can_read_metadata` | View resource metadata | | `can_manage_metadata` | Modify resource metadata | | `can_manage_projects` | Create/delete projects (organization-level) | | `can_manage_users` | Manage user memberships (organization-level) | ## Permission Inheritance The authorization model uses OpenFGA's `from` keyword to implement hierarchical permission inheritance: ```text theme={null} Projects inherit from Organizations Resources inherit from Projects Resource permissions can compute from Project permissions (e.g., can_delete from project) ``` This means an organization `admin` automatically has appropriate access to all projects and resources within their organization, without needing explicit tuples on each resource: ```bash theme={null} # This single tuple: group:org-admins admin organization:acme-corp # Grants (via inheritance): # - can_read on all projects in acme-corp # - can_write on all projects in acme-corp # - can_delete on all projects in acme-corp # - can_read on all resources in those projects # - can_write on all resources in those projects # - ... and so on ``` The inheritance chain is established via parent link tuples: ```yaml theme={null} project:analytics#organization@organization:acme-corp data_connection:pg-prod#project@project:analytics ``` ### Computed delegation: `can_delete from project` For resource types like `data_connection`, the schema computes resource-level permissions from project-level permissions. For example, `can_delete` on a data connection is satisfied if the user has delete rights on the parent project: ```text theme={null} type data_connection relations define project: [project] define can_delete: owner or admin or can_delete from project define can_execute: operator or developer or admin or owner or can_execute from project ``` This means an organization owner who has `can_delete` on every project (via the org-from-project chain above) can also delete every project-scoped resource without per-resource tuples. ### Service worker permissions on `data_connection` Service workers (background tasks like the data-readiness worker, scheduled enrichment runs, and dataset sync jobs) need to invoke data connections. The schema grants `can_execute` on `data_connection` to the service-worker role at the project level so workers can `POST /assets/data/{resource_uri}/verify` and `GET /assets/data/{resource_uri}/secret` against any connection in their assigned project, without needing a per-connection tuple. This follows the same pattern as `can_create_resources` on `project` for service workers: a single role binding at the parent grants narrow operational permissions across all child resources. ## Authorization Schema (OpenFGA DSL) The schema is defined in `openfga-schema.fga` and compiled to `openfga-schema.json` for runtime use. Here is a simplified excerpt showing the project type: ```text theme={null} type project relations define organization: [organization] # Direct assignments define owner: [user, group#member, role#assignee] define admin: [user, group#member, role#assignee] define developer: [user, group#member, role#assignee] define operator: [user, group#member, role#assignee] define viewer: [user, group#member, role#assignee] # Computed permissions with inheritance define can_read: viewer or operator or developer or admin or owner or admin from organization or owner from organization define can_write: operator or developer or admin or owner or admin from organization or owner from organization define can_delete: owner or admin or owner from organization or admin from organization define can_create_resources: developer or admin or owner or admin from organization or owner from organization ``` When modifying the OpenFGA schema, always update both the `.fga` DSL file and regenerate the `.json` runtime file by running `./scripts/generate-openfga-schema.sh`. The Governance service reads only the JSON file at startup. Adding new permissions is safe; renaming or removing is a **breaking change**. ## The require\_permission Pattern All API endpoints use the `require_permission()` dependency to enforce authorization. This is implemented in the shared `common` package and used consistently across all services: ```python theme={null} from common.permissions import Permissions, require_permission @router.delete("/data/{resource_uri}") async def delete_data_connection( resource_uri: str, _: None = Depends(require_permission( Permissions.DATA_CONNECTION, # Resource type Permissions.DATA_CONNECTION.actions.CAN_DELETE, # Required permission "resource_uri" # Path parameter with resource ID )) ): ... ``` ```python theme={null} from common.permissions import Permissions, require_permission, HEADER_PROJECT_ID @router.post("/data") async def create_data_connection( payload: DataConnectionCreateRequest, _: None = Depends(require_permission( Permissions.PROJECT, # Check on the project Permissions.PROJECT.actions.CAN_CREATE_RESOURCES, # Required permission HEADER_PROJECT_ID # From X-Project-ID header )) ): ... ``` ```python theme={null} from common.permissions import require_permission_dynamic @router.get("/{resource_type}/{resource_id}/secrets") async def list_secrets( resource_type: str, resource_id: str, _: None = Depends(require_permission_dynamic( resource_type_param="resource_type", # Resolved from path parameter permission="can_read_secrets", resource_id_param="resource_id" )) ): ... ``` ## Permission Check Flow When `require_permission()` is invoked, the following occurs: The resource ID is resolved from path parameters, query parameters, or headers based on the configuration. The `Auth` object is extracted from the validated JWT, providing `sub` (user ID) and `teams` (group memberships). The permission checker queries OpenFGA with: * **User:** `user:{sub}` and each `group:{group_name}#member` * **Relation:** The computed permission (e.g., `can_delete`) * **Object:** `{resource_type}:{resource_id}` If any user or group check returns `true`, access is granted. * **200/success:** Permission granted, the endpoint handler executes * **403 Forbidden:** Permission denied, the request is rejected * **500 Internal Server Error:** Permission system misconfigured ## Permission API The Governance service exposes a Permissions API for programmatic permission management: | Endpoint | Method | Description | | ------------------------------------ | ------ | ------------------------------------------------------- | | `/governance/permissions/check` | GET | Check if a user has a specific permission on a resource | | `/governance/permissions/grant` | POST | Grant a relation to a user or group on a resource | | `/governance/permissions/revoke` | POST | Revoke a relation from a user or group | | `/governance/permissions/set-parent` | POST | Establish a parent-child relationship for inheritance | | `/governance/permissions/delete-all` | POST | Remove all permission tuples for a resource (cleanup) | Assets and Utils never call the Permissions API directly. They use the `require_permission()` dependency, which internally forwards the user's JWT to the Governance service for permission validation via the auto-generated SDK. ## Keycloak Groups and OpenFGA The bridge between Keycloak (identity) and OpenFGA (authorization) is **Keycloak groups**: 1. Users are assigned to groups in Keycloak (e.g., `org-admins`, `project-developers`) 2. A groups mapper on the realm ensures group memberships appear in the JWT `groups` claim 3. During bootstrap, permission tuples map groups to OpenFGA relations (e.g., `group:org-admins#member` -> `admin` on `organization`) 4. At runtime, `require_permission()` checks both the user's direct tuples and their group-based tuples This means adding a user to a Keycloak group is sufficient to grant them the corresponding platform permissions -- no separate OpenFGA management is needed. ## Next Steps Understand how JWT tokens carry group claims for authorization. See how organization-level permissions are bootstrapped. Learn about project-level role assignments and inheritance. Explore the full tenant isolation architecture. # Data Connections Source: https://docs.emergence.ai/platform/data-connections Manage external data source connections with secure credential storage, connection verification, and fine-grained access control. # Data Connections Data connections represent configured links to external data sources: databases, object storage, and file systems. They are managed by the **Assets** service (port 8000) and provide a secure, unified interface for solutions to discover and access external data without handling raw credentials. Connections can be scoped to a single project or to the entire organization (org-level). See [Org-level data connections](#org-level-data-connections) below. Credentials for data connections are stored securely via the Governance Secrets API (backed by Infisical or ESO + GCP Secret Manager). The connection record itself stores only metadata; credentials are never persisted in the Assets database. ## Supported Connection Types | Type | Status | Description | | ------------ | ----------- | --------------------------------------------------------------------------------------------------------------- | | `postgres` | Implemented | PostgreSQL databases | | `redshift` | Implemented | Amazon Redshift data warehouses | | `snowflake` | Implemented | Snowflake data warehouses (Basic + JWT/key-pair auth available; OAuth and SSO planned based on customer demand) | | `mysql` | Planned | MySQL databases | | `bigquery` | Planned | Google BigQuery datasets | | `databricks` | Planned | Databricks SQL warehouses | | `couchbase` | Planned | Couchbase NoSQL databases | | `oracle` | Planned | Oracle databases | | `hive` | Planned | Apache Hive metastore | | `mongodb` | Planned | MongoDB NoSQL databases | Database connections support: * Schema and table discovery during verification (note: Snowflake performs server-level discovery and does not enumerate schemas/tables ahead of time) * Selected table filtering * Connection pooling configuration | Type | Status | Description | | ------- | ----------- | --------------------------------- | | `s3` | Implemented | Amazon S3 or S3-compatible stores | | `gcs` | Implemented | Google Cloud Storage | | `minio` | Implemented | Self-hosted MinIO instances | Storage connections support: * Bucket and prefix configuration * Credential verification * Multiple authentication methods (access key, service account, etc.) | Type | Status | Description | | ----- | ----------- | ------------------------ | | `nfs` | Implemented | NFS-mounted file systems | | `smb` | Planned | SMB/CIFS network shares | File system connections support: * Path-based configuration * Permission validation ## Connection Lifecycle Query `GET /assets/data/types` to retrieve all connection types with their credential schemas, authentication methods, and example configurations. Use `POST /assets/data/verify` to test credentials without creating a connection. For databases, this also discovers available schemas and tables for selection. `POST /assets/data` creates the connection record, stores credentials as a secret via the Governance Secrets API, and sets up ownership permissions in OpenFGA. Solutions retrieve connection metadata via `GET /assets/data/{resource_uri}` and fetch decrypted credentials via `GET /assets/data/{resource_uri}/secret` when executing queries. `POST /assets/data/{resource_uri}/verify` tests that stored credentials are still valid and the data source is accessible. ## Resource URIs Data connections use **resource URIs** as identifiers instead of UUIDs. Resource URIs follow a structured format: ```yaml theme={null} data:{org_id}:{project_id}:{connection_name} ``` For example: `data:acme-corp:analytics-prod:customer-db` If a `resource_uri` is not provided during creation, one is auto-generated from the connection name. ## Org-level data connections Data connections can be scoped to a single project (the default) or to the entire organization. Org-level connections let multiple projects share the same configured data source without per-project duplication. **Creating an org-level connection:** omit the `X-Project-ID` header (or pass `project_id=null` in the request body where supported). The created connection has its `project_id` field unset and lives at the org boundary. **Resource URI shape:** org-level connections drop the project segment. ```yaml theme={null} data:{org_id}::{connection_name} # org-level (note the empty project segment) data:{org_id}:{project_id}:{connection_name} # project-scoped (default) ``` **Permission semantics:** an org-level connection's permissions are evaluated against the organization, not a project. The OpenFGA computed-permission graph extends `can_*` checks across the org so that: * Org owners can read, write, and delete org-level connections. * Members of any project under the organization can `read` and `execute` an org-level connection if their project role grants those permissions on org-shared resources. (See [Authorization](/platform/authorization) for the full chain.) * Cross-project visibility is the explicit point of org-level connections; standard project-scoped connections remain isolated. **When to use:** a database that powers analytics across all projects in the organization, a shared S3 bucket for raw data drops, or any connection where per-project credential management would create operational drag without a security benefit. ## API Reference All endpoints require the `X-Project-ID` header and a valid JWT token. Lists all available connection types with credential schemas. Returns each connection type with its category, description, supported authentication methods, required credential fields, and example configurations. Use this to build dynamic connection forms. Lists data connections in the current project with filtering and pagination. **Access:** `can_read` on the project. **Query Parameters:** * `page`, `limit`: Pagination * `search`: Search in name and description. Whitespace-only search terms return HTTP 400. `%` and `_` characters are treated as literal text (ILIKE-escaped) so a search for `customer_db` matches the literal string, not "customer" + any character + "db". * `connection_type`: Filter by type (postgres, s3, etc.) * `status`: Filter by status (PENDING, ACTIVE, ERROR, DISABLED) * `tags`: Comma-separated tag filter (AND logic) * `sort`: Sort specification (e.g., `name:asc,created_at:desc`) * `owner_id`: Filter by owner Verifies connection credentials before saving. Tests connectivity, validates permissions, and discovers available schemas and tables for databases. **Access:** `can_create_resources` on the project. Currently supports verification for: PostgreSQL, Redshift, S3, GCS, MinIO. Creates a new data connection with inline credentials. **Access:** `can_create_resources` on the project. **Side effects:** * Creates the data connection record in the database * Auto-generates `resource_uri` from name if not provided * Stores credentials as a secret via the Governance Secrets API * Creates an ownership tuple in OpenFGA ```json Request Body theme={null} { "name": "Customer Analytics DB", "connection_type": "postgres", "description": "Production read replica for analytics", "database_config": { "host": "pg-replica.example.com", "port": 5432, "database": "analytics", "schema": "public", "selected_tables": ["customers", "orders", "products"] }, "credentials": { "username": "analytics_reader", "password": "secret" }, "tags": ["production", "analytics"] } ``` Retrieves data connection details (metadata only, no credentials). **Access:** `can_read` on the data connection. Updates a data connection. The `resource_uri` cannot be changed. **Access:** `can_write` on the data connection. Credentials can be updated by providing new values. If credentials are omitted, existing credentials are preserved. Deletes a data connection and all associated resources. **Access:** `can_delete` on the data connection. **Side effects:** * Removes the data connection record * Deletes OpenFGA permission tuples * Deletes associated secrets from the Governance Secrets API Retrieves decrypted credential values for a data connection. **Access:** `can_execute` on the data connection. This elevated permission ensures only authorized automation (agents, workflows) can access raw credentials. Secrets are never cached and access is logged for audit purposes. Verifies that an existing connection's stored credentials are still valid. **Access:** `can_execute` on the data connection. ## Credential Storage Architecture Data connection credentials follow a layered storage model: ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD A["Assets Service (port 8000)
Data connection record: metadata, config, status
Secret names: [credentials]"] G["Governance Service (port 8000)
Secrets API"] S["Secrets Backend
(Infisical or ESO + GCP Secret Manager)
Encrypted storage · Audit logging · OpenFGA access control"] A -->|"Governance SDK call
(forwarding user JWT)"| G G --> S classDef service fill:#E69F00,stroke:#555555,color:#000 classDef secrets fill:#009E73,stroke:#555555,color:#000 class A,G service class S secrets ``` The Assets service never stores or caches credentials. Every secret retrieval goes through the Governance Secrets API, which enforces its own permission checks and audit logging. ## Data Connection Permissions Data connections support the full set of resource-level permissions: | Permission | Who Can | Use Case | | --------------------- | -------------------------------------------------- | -------------------------------------- | | `can_read` | Viewers, operators, developers, owners + inherited | View connection metadata | | `can_write` | Operators, developers, owners + inherited | Update connection settings | | `can_delete` | Developers, owners + project admin/owner | Remove connections | | `can_execute` | Operators, developers, owners + inherited | Retrieve credentials, run verification | | `can_read_secrets` | Operators, developers, owners + inherited | View associated secrets | | `can_manage_secrets` | Developers, owners + inherited | Update credentials | | `can_read_metadata` | Operators, developers, owners + inherited | View data connection metadata | | `can_manage_metadata` | Developers, owners + inherited | Update metadata and tags | ## Next Steps Learn how agents use data connections to access external data. Understand how data connections are scoped to projects. Explore the permission model for data connections. Automate data connection workflows with scheduled tasks. # LLM Gateway Source: https://docs.emergence.ai/platform/llm-gateway Operator guide for the shared LiteLLM gateway — the sole authority for agent LLM access, model allowlist, rate limits, authentication, and observability # LLM Gateway The CRAFT platform routes agent LLM traffic through a **shared LiteLLM gateway** — a single cluster-level service that holds provider credentials centrally. This page describes the target architecture from the Agent Security ADR and the operator contract for configuring the gateway. This page is for platform operators and solution-team leads. Solution developers looking for how to call an LLM from their solution code should see [Access LLMs](/guides/solution-dev/access-llms) instead. **Target vs. current deployment.** The shared gateway is the recommended model. Current em-talk2data Helm charts do not bundle a managed gateway — agents in those deployments invoke the LiteLLM SDK in-process against Vertex/Bedrock directly. Operators adopting the gateway pattern must deploy it separately and set `OPENAI_API_BASE` / `LITELLM_API_BASE` on agent deployments. ## Architecture ### Shared gateway model (Agent Security ADR) Per the Agent Security ADR, agents are **untrusted execution environments**. The gateway enforces this through two architectural constraints: 1. **No credentials in agent pods.** Provider API keys (OpenAI, Anthropic, Google) live only in the LLM Gateway's environment. Agents never receive or handle these secrets. 2. **All model access is brokered.** Every completion request passes through the gateway's allowlist check, budget enforcement, and audit pipeline before reaching a provider. The LiteLLM gateway is deployed as a **shared Kubernetes Deployment** — one set of gateway replicas serves all agents in the cluster. Agents call it via its internal ClusterIP service address. **Identity sidecar vs. LLM gateway.** The Agent Security ADR also describes a per-agent **identity sidecar** — a lightweight container that holds the workload's Kubernetes ServiceAccount JWT and authenticates the agent to platform gateways (including the LLM Gateway) using short-lived scoped tokens. This is separate from the LLM gateway itself. The identity sidecar is a per-pod security boundary; the LLM gateway is a shared cluster service. ### How requests flow ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR USER["User / orchestrator"] AGENT["Agent pod\n(no provider keys)"] IDSIDECAR["Identity sidecar\n(K8s SA JWT)"] GW["LLM Gateway\n(shared, litellm proxy)"] ALLOW["Allowlist + budget\ncheck"] ROUTE["Provider routing"] LANGFUSE["Langfuse\n(traces + cost)"] VERTEX["Google Vertex AI"] ANTH["Anthropic"] SELFHOST["Self-hosted\n(Ollama / vLLM)"] USER --> AGENT AGENT -->|"LLM_GATEWAY_URL\n(ClusterIP)"| IDSIDECAR IDSIDECAR -->|"scoped token\n+ request"| GW GW --> ALLOW ALLOW -->|"allowed"| ROUTE ALLOW -->|"denied → 403"| IDSIDECAR GW -.auto-traces.-> LANGFUSE ROUTE --> VERTEX ROUTE --> ANTH ROUTE --> SELFHOST classDef user fill:#56B4E9,stroke:#555555,color:#000 classDef agent fill:#E69F00,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef decision fill:#F0E442,stroke:#555555,color:#000 classDef store fill:#009E73,stroke:#555555,color:#000 classDef ext fill:#CC79A7,stroke:#555555,color:#000 class USER user class AGENT agent class IDSIDECAR infra class GW infra class ALLOW decision class ROUTE infra class LANGFUSE store class VERTEX,ANTH,SELFHOST ext ``` **Credential isolation in practice:** the LLM Gateway pod holds `VERTEXAI_PROJECT`, `ANTHROPIC_API_KEY`, etc. The agent pod's only gateway-related secrets are `LLM_GATEWAY_URL` (a ClusterIP service address) and `LLM_GATEWAY_API_KEY` (a scoped gateway key with no provider privilege). Neither reveals which provider is in use, and neither can be used to call a provider directly. ### em-runtime-mcp as agent tool gateway `em-runtime-mcp` is the **tool-call gateway** — the single endpoint through which every agent invokes a platform tool. The LLM gateway and `em-runtime-mcp` are complementary: | Gateway | Handles | Enforces | | --------------------- | ------------------- | ---------------------------------------------- | | LLM Gateway (LiteLLM) | LLM completions | Model allowlist, rate limits, cost attribution | | `em-runtime-mcp` | Platform tool calls | Per-agent tool allowlist, audit events | ## Model registry ### Allowlist configuration The gateway's `config.yaml` defines the models any agent in the cluster is permitted to call. Requests for unlisted models return `400 Model not allowed`. ```yaml theme={null} # LLM Gateway config.yaml (mounted as a ConfigMap on the gateway Deployment). # CRAFT defaults route to Vertex AI; the gateway is provider-agnostic, so other # providers (OpenAI, Anthropic direct, self-hosted) can be added side-by-side. model_list: # DEFAULT for most solutions and agents. # GA on Vertex AI Model Garden (since Google I/O 2026-05-19). Outperforms # gemini-3.1-pro on 6 of 8 major benchmarks — including agentic (MCP Atlas) # and coding (Terminal-Bench) — at 3.6× the speed and a fraction of the cost. - model_name: gemini-3.5-flash litellm_params: model: vertex_ai/gemini-3.5-flash vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global # Specialized variant. Use ONLY when custom tool calling is a core # requirement (the model prioritizes registered custom functions over bash # fallbacks). Per Google's own guidance: if >50% of requests don't involve # tool calling, stay on gemini-3.5-flash. Preview SLA; global endpoint only. - model_name: gemini-3.1-pro-preview-customtools litellm_params: model: vertex_ai/gemini-3.1-pro-preview-customtools vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global # Secondary / cross-provider option — demonstrates model agnosticity - model_name: claude-opus-4-8 litellm_params: model: vertex_ai/claude-opus-4-8 vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global # Self-hosted (no key required) - model_name: ollama/llama3.1 litellm_params: model: ollama/llama3.1 api_base: http://ollama.ollama.svc.cluster.local:11434 litellm_settings: drop_params: true # tolerate extra params from different SDKs request_timeout: 120 # seconds general_settings: master_key: os.environ/LITELLM_MASTER_KEY ``` Agents call `litellm.acompletion(model="openai/gemini-3.5-flash", api_base=LLM_GATEWAY_URL, ...)` using the `model_name` alias. Switching providers is a gateway config change — no agent code changes required. **Use `gemini-3.1-pro-preview-customtools` only when extensive custom tool calling is core to the agent.** It's a fine-tuned variant that prioritizes registered custom functions over bash fallbacks. Google's own guidance: if > 50% of requests don't involve tool calling, stay on `gemini-3.5-flash` — > quality on non-tool workloads is lower on the customtools variant. Three operational constraints: **(1) global-endpoint only** — `vertex_location` must be `global`. **(2) Lower quota** — configure a fallback chain to `gemini-3.5-flash` or `claude-opus-4-8`. **(3) Preview SLA** — may be deprecated or renamed without GA notice. **Environment-variable conventions.** The gateway reads Vertex credentials from `VERTEXAI_PROJECT` and `VERTEXAI_LOCATION` (LiteLLM's documented convention, matching the em-talk2data Helm chart values). LiteLLM also accepts Google Cloud SDK standards `GOOGLE_CLOUD_PROJECT` + `GOOGLE_CLOUD_LOCATION`. Authenticate the gateway pod to Vertex AI via Workload Identity (production) or `GOOGLE_APPLICATION_CREDENTIALS` pointing to an ADC JSON (local dev). ### Provider routing | Prefix in `litellm_params.model` | Provider | | -------------------------------- | ---------------------------------- | | `vertex_ai/` | Google Vertex AI | | `openai/` | OpenAI | | `anthropic/` | Anthropic (direct) | | `gemini/` | Google Gemini (direct, non-Vertex) | | `ollama/` | Local Ollama | | `vllm/` | vLLM endpoint | | `azure/` | Azure OpenAI | To add fallbacks, list multiple entries sharing the same `model_name`: ```yaml theme={null} - model_name: gemini-3.5-flash litellm_params: model: vertex_ai/gemini-3.5-flash vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global - model_name: gemini-3.5-flash # fallback for resilience litellm_params: model: vertex_ai/claude-opus-4-8 vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global ``` ## Authentication ### Gateway API key The gateway enforces a `master_key`. Each project's agents receive a distinct scoped key provisioned by the platform secrets pipeline. The key grants access to the gateway — it carries no provider-level privilege and cannot be used to call a model provider directly. Agents receive the key via their Helm values: ```yaml theme={null} # charts/your-agent/values.yaml env: - name: LLM_GATEWAY_URL value: "http://llm-gateway.craft.svc.cluster.local/v1" - name: LLM_GATEWAY_API_KEY valueFrom: secretKeyRef: name: llm-gateway-keys key: project-key ``` The gateway's own provider secrets (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `VERTEXAI_PROJECT`, etc.) are mounted into the **gateway Deployment only** and are never visible to agent pods. ## Budget enforcement ### Rate limits ```yaml theme={null} router_settings: num_retries: 3 timeout: 120 retry_after: 5 # seconds between retries model_list: - model_name: gemini-3.5-flash litellm_params: model: vertex_ai/gemini-3.5-flash vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global tpm: 100000 # tokens per minute (cluster-wide) rpm: 500 # requests per minute (cluster-wide) ``` Limits are enforced **cluster-wide** at the shared gateway, not per-agent. When a limit is exceeded, the gateway returns `429 Too Many Requests`; clients back off exponentially. ### Cost ceilings ```yaml theme={null} litellm_settings: max_budget: 50.00 # USD, per model alias budget_duration: monthly ``` ### Overage behavior 1. Gateway returns `429` with a `Retry-After` header. 2. Agent catches `429` and applies exponential back-off with jitter. 3. If the alias has a fallback in `model_list`, the router tries it automatically. 4. If no fallback and budget exhausted, `429` propagates to the caller. Do not remove the `fallbacks` list from the router config without first confirming the upstream has headroom. A saturated primary with no fallback causes a complete LLM outage for **all agents in the cluster** — not just one pod. ## Observability ### Langfuse traces LiteLLM auto-emits traces to Langfuse when these env vars are set on the **gateway Deployment**: ```bash theme={null} LANGFUSE_HOST=https://langfuse.your-cluster.example.com LANGFUSE_PUBLIC_KEY= LANGFUSE_SECRET_KEY= ``` Enable in the gateway config: ```yaml theme={null} litellm_settings: success_callback: ["langfuse"] failure_callback: ["langfuse"] ``` Every completion call generates a Langfuse trace tagged with the `metadata` the agent passes (`project_id`, `solution`, `trace_id`) — the basis for per-project cost attribution dashboards. ### Prometheus and OpenTelemetry metrics LiteLLM exposes `/metrics` (Prometheus format) on port 4000. The platform OTEL Collector scrapes it and forwards to your observability backend. | Metric | Description | | ---------------------------------- | --------------------------------------------- | | `litellm_requests_total` | Total completion requests by model and status | | `litellm_tokens_total` | Total tokens consumed by model | | `litellm_request_duration_seconds` | Latency histogram | | `litellm_spend_usd` | Cumulative spend by model alias | OTLP export from the gateway: ```yaml theme={null} litellm_settings: service_callback: ["otel"] environment_variables: OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317" OTEL_SERVICE_NAME: "llm-gateway" ``` ## Provider configuration ```yaml theme={null} - model_name: gemini-3.5-flash litellm_params: model: vertex_ai/gemini-3.5-flash vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global - model_name: gemini-3.1-pro-preview-customtools litellm_params: model: vertex_ai/gemini-3.1-pro-preview-customtools vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global # preview model — global endpoint only ``` Authenticate the gateway with Workload Identity (GKE) or a mounted ADC JSON (`GOOGLE_APPLICATION_CREDENTIALS`). `gemini-3.5-flash` is the recommended fast tier. ```yaml theme={null} - model_name: claude-opus-4-8 litellm_params: model: vertex_ai/claude-opus-4-8 vertex_project: os.environ/VERTEXAI_PROJECT vertex_location: global ``` Anthropic models served by Vertex AI Model Garden. Stays within the Vertex tenancy boundary — no separate Anthropic key required. ```yaml theme={null} - model_name: gpt-5.5 litellm_params: model: openai/gpt-5.5 api_key: os.environ/OPENAI_API_KEY ``` Mount `OPENAI_API_KEY` from a Kubernetes Secret into the **gateway Deployment only**. ```yaml theme={null} - model_name: claude-opus-4-8 litellm_params: model: anthropic/claude-opus-4-8 api_key: os.environ/ANTHROPIC_API_KEY ``` Mount `ANTHROPIC_API_KEY` from a Kubernetes Secret into the **gateway Deployment only**. ```yaml theme={null} - model_name: gemini-3.5-flash litellm_params: model: gemini/gemini-3.5-flash api_key: os.environ/GEMINI_API_KEY ``` Prefer the Vertex AI entry above for production — it inherits the tenant's IAM boundary rather than a long-lived API key. ```yaml theme={null} - model_name: llama3.1 litellm_params: model: ollama/llama3.1 api_base: http://ollama.ollama.svc.cluster.local:11434 ``` No API key required. The Ollama service must be reachable from the gateway namespace. ```yaml theme={null} - model_name: llama3.1-70b litellm_params: model: openai/llama3.1-70b api_base: http://vllm.vllm.svc.cluster.local:8000/v1 api_key: os.environ/VLLM_API_KEY ``` vLLM exposes an OpenAI-compatible API — use the `openai/` prefix. ## Deploying the gateway The LLM Gateway is a Kubernetes Deployment behind a ClusterIP Service. It is **not** a sidecar in each agent pod — it is a shared platform service maintained by the platform team. ```yaml theme={null} # Minimal production Deployment (add HPA + PodDisruptionBudget for HA) apiVersion: apps/v1 kind: Deployment metadata: name: llm-gateway namespace: craft spec: replicas: 2 # scale horizontally for throughput; add Redis for shared state selector: matchLabels: app: llm-gateway template: spec: containers: - name: litellm image: ghcr.io/berriai/litellm:main-latest ports: - containerPort: 4000 env: - name: LITELLM_MASTER_KEY valueFrom: secretKeyRef: name: llm-gateway-secrets key: master-key - name: VERTEXAI_PROJECT valueFrom: secretKeyRef: name: llm-gateway-secrets key: vertex-project volumeMounts: - name: config mountPath: /app/config.yaml subPath: config.yaml volumes: - name: config configMap: name: llm-gateway-config --- apiVersion: v1 kind: Service metadata: name: llm-gateway namespace: craft spec: selector: app: llm-gateway ports: - port: 80 targetPort: 4000 type: ClusterIP ``` Agents in any namespace reference the gateway as `http://llm-gateway.craft.svc.cluster.local/v1`. ## Disaster recovery — gateway unavailable Because the gateway is shared, a gateway outage affects all agents cluster-wide, not a single pod. **Symptoms:** agents receive `Connection refused` or `503 Service Unavailable` on their gateway URL. **Response:** 1. Check gateway pod health: `kubectl get pods -n craft -l app=llm-gateway` 2. Check gateway logs: `kubectl logs -n craft deployment/llm-gateway --tail=50` 3. Check provider upstream status (Vertex, OpenAI, Anthropic status pages) 4. If gateway pods are crash-looping: `kubectl rollout restart deployment/llm-gateway -n craft` 5. If a model alias is hitting rate limits, the fallback entry in `model_list` activates automatically To verify the gateway is healthy: ```bash theme={null} curl http://llm-gateway.craft.svc.cluster.local/health | jq . # Expected: {"status": "healthy"} ``` ## Next steps How to call the gateway from solution code using litellm. Platform-side observability: Langfuse traces, cost dashboards, model comparison. How provider keys and gateway keys flow through the secrets pipeline. How the gateway fits into the overall platform architecture. Connect Claude Code, Cursor, Goose, or an external agent to CRAFT's tool gateway over MCP. # MCP Server Source: https://docs.emergence.ai/platform/mcp-server Connect AI clients and external agents to CRAFT's tool gateway over the Model Context Protocol — IDE integrations, headless agent access, and tool reference # MCP Server CRAFT handles the **"what"** — governed, accurate data retrieval from your enterprise systems — so your AI tools and agents can focus on the **"why"**: validating hypotheses, identifying root causes, running what-if scenarios, and surfacing anomalies. Use `resolve_term` so your agent knows what "active customer" means in *your* warehouse before generating SQL; use `execute_query` to validate results; use `generate_plotly_chart` to surface patterns instantly. Connect Claude Code, Cursor, Goose, or a purpose-built agent to `https://craft.emergence.ai/mcp` and CRAFT becomes a governed data layer for any reasoning workflow. Two connection patterns: **(a) interactive** — IDE clients query CRAFT while you code; **(b) programmatic** — embed CRAFT as a tool server in an agent pipeline for hypothesis testing, root cause analysis, anomaly detection, or what-if modeling. ## Server details | Property | Value | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint | `https://craft.emergence.ai/mcp` | | Transport | Streamable HTTP ([MCP 2025-11-25 spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports); SSE-compat path available) | | Auth | OAuth 2.0 PKCE (authorization code + S256, via Keycloak) | | Health | `GET https://craft.emergence.ai/health` → `{"status": "ok"}` | ## Verify the endpoint (no auth needed) Before setting up OAuth, confirm the server is reachable with the no-auth `hello_world` tool: ```bash theme={null} curl -s -X POST https://craft.emergence.ai/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hello_world","arguments":{}}}' \ | jq . ``` Expected response: ```json theme={null} {"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": "Hello from CRAFT MCP server"}]}} ``` If this succeeds, network connectivity and DNS are confirmed. If you prefer a CLI tool: ```bash theme={null} npx @mcpjam/cli server doctor --url https://craft.emergence.ai/mcp ``` ## Authentication ### Interactive clients (OAuth) Supported IDE clients use the [MCP OAuth 2.0 PKCE authorization flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). On first connection, the client opens a browser window to `craft.emergence.ai`, where you sign in with your **Google** or **Microsoft** account. The access token is stored and refreshed by the client automatically — no manual token handling required. ### X-Project-ID header All tools except `hello_world` require an `X-Project-ID` header scoping the request to your CRAFT project. Find your project ID in the CRAFT console under **Settings → Project → ID**. `X-Project-ID` selects a project scope but does **not** grant access — project membership is enforced server-side against your JWT's claims. Passing an arbitrary project ID returns an authorization error if you are not a member. ### Token requirements for headless callers Programmatic callers (agents, pipelines) must obtain an access token before calling tools: | Requirement | Value | | --------------- | ------------------------------------------------------------------------ | | Issuer (`iss`) | `https://craft.emergence.ai/keycloak/realms/emergence` | | Required scopes | `openid profile email` | | Grant type | Authorization code (interactive) or Client credentials (service account) | | Token TTL | \~5 minutes; refresh token manages rotation for interactive clients | For service accounts and CI pipelines, see [Service Accounts](/platform/service-accounts) and [Authentication](/platform/authentication). ## Connect your AI client Add the CRAFT MCP server from the CLI: ```bash theme={null} claude mcp add --transport http craft https://craft.emergence.ai/mcp \ --header "X-Project-ID: " ``` Or add it to `.mcp.json` in your project root (or `~/.claude.json` for user-scope): ```json theme={null} { "mcpServers": { "craft": { "type": "http", "url": "https://craft.emergence.ai/mcp", "headers": { "X-Project-ID": "" } } } } ``` The OAuth browser flow triggers automatically on first tool call. Token refresh is handled by Claude Code. Find your project ID in the CRAFT console under **Settings → Project → ID**. Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` globally): ```json theme={null} { "mcpServers": { "craft": { "type": "http", "url": "https://craft.emergence.ai/mcp", "headers": { "X-Project-ID": "${env:CRAFT_PROJECT_ID}" } } } } ``` Set `CRAFT_PROJECT_ID` as a shell environment variable — Cursor expands `${env:VAR}` at runtime. The OAuth browser window opens on first use. Create `.vscode/mcp.json` in your project: ```json theme={null} { "inputs": [ { "type": "promptString", "id": "craft-project-id", "description": "Your CRAFT project ID" } ], "servers": { "craft": { "type": "http", "url": "https://craft.emergence.ai/mcp", "headers": { "X-Project-ID": "${input:craft-project-id}" } } } } ``` VS Code prompts for the project ID once and stores it securely. OAuth handles the access token automatically. If tool calls fail silently, headers may not be forwarded — a known issue in some VS Code versions ([#264095](https://github.com/microsoft/vscode/issues/264095)). Workaround: replace the `type`/`url` block with `mcp-remote`: ```json theme={null} { "servers": { "craft": { "command": "npx", "args": [ "mcp-remote@latest", "https://craft.emergence.ai/mcp", "--header", "X-Project-ID: " ] } } } ``` Note: `mcp-remote` handles the OAuth flow locally rather than delegating it to the server. Add to `~/.config/goose/config.yaml`: ```yaml theme={null} extensions: craft: name: craft type: http url: https://craft.emergence.ai/mcp enabled: true headers: X-Project-ID: "${CRAFT_PROJECT_ID}" ``` Set `CRAFT_PROJECT_ID` in your environment. Alternatively, run `goose configure`, select **Remote Extension (Streamable HTTP)**, and the interactive wizard will prompt you for the URL and any custom headers. OAuth triggers on first connection. ## Interactive charts — MCP Apps extension `generate_plotly_chart` returns both a raw `plotly_json` response (all clients) and an embedded interactive UI resource for clients that support the [MCP Apps protocol](https://modelcontextprotocol.io/extensions/apps/overview). The embedded resource is a sandboxed iframe containing the full Plotly chart with a modebar — including an **SVG download** button — rendered inline alongside the chat without switching tabs. The interactive iframe is enabled by default (`MCP_APPS_ENABLED=true`). No client-side configuration is required; supported clients detect and render it automatically. Clients that support MCP Apps interactive chart rendering (verified 2026-05-28): | Client | MCP Apps | Notes | | ---------------------------- | :------: | --------------------------------------------------------- | | Cursor 2.6+ | ✅ | Desktop IDE; renders interactive Plotly iframe inline | | VS Code Copilot Chat v1.109+ | ✅ | Browser-rendered panel with modebar | | Goose | ✅ | Desktop app; renders iframes inline | | Postman | ✅ | Renders in Postman canvas; good for dev/testing | | MCPJam | ✅ | Inspector UI; best for debugging chart output | | Claude Code (CLI) | ❌ | Terminal — no browser context; receives raw `plotly_json` | Claude Code and other non-Apps clients receive the raw `plotly_json` response. Render it locally with `plotly.io.show(fig)` in Python or `Plotly.newPlot(...)` in JavaScript. To disable the embedded resource globally, set `MCP_APPS_ENABLED=false` on the Talk2Data service. ## Developer tools Use these tools to test and debug CRAFT's MCP server during development, especially the MCP Apps / Plotly rendering path. [MCPJam](https://app.mcpjam.com) is a dedicated MCP inspector — web app (no install), desktop app, or CLI. It supports the MCP Apps extension and renders interactive Plotly charts inline. **Web app or desktop**: Add a server with URL `https://craft.emergence.ai/mcp`, enable OAuth, and set `X-Project-ID` in the headers field: ```json theme={null} { "name": "craft", "type": "http", "url": "https://craft.emergence.ai/mcp", "headers": { "X-Project-ID": "" }, "useOAuth": true } ``` **CLI**: Run a health check and OAuth flow validation: ```bash theme={null} npx @mcpjam/cli server doctor \ --url https://craft.emergence.ai/mcp \ --header "X-Project-ID: " ``` MCPJam provides full JSON-RPC trace visibility and guided OAuth conformance checks (DCR, PKCE, CIMD) — useful for validating the MCP server before shipping tool changes. Postman can act as an MCP host (client), connecting to external MCP servers — distinct from Postman's own MCP server for managing Postman resources. Go to **Settings → MCP Hosts → Add** and configure: * **URL**: `https://craft.emergence.ai/mcp` * **Auth**: OAuth (recommended) or access token * **Headers**: `X-Project-ID: ` Postman supports the MCP Apps extension — Plotly charts appear inline in the Postman canvas. Once you have a working configuration, use Postman's **Export MCP config** button to generate a ready-to-paste config for Claude Code, Cursor, or VS Code. ## Calling CRAFT from an external agent Any MCP-compatible agent framework can use CRAFT as a tool server, bringing governed data access to any reasoning pipeline. ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR EA["External agent\n(any framework)"] GW["gateway-external\n(public HTTPS)"] MCP["em-runtime-mcp\n(tool gateway)"] ALLOW["Allowlist check\n+ audit event"] CATALOG["Catalog tools\nlist · get · search"] T2D["Talk2Data tools\nSQL · query · chart"] EA -->|"Streamable HTTP\nOAuth + X-Project-ID"| GW GW --> MCP MCP --> ALLOW ALLOW --> CATALOG ALLOW --> T2D classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef decision fill:#F0E442,stroke:#555555,color:#000 classDef action fill:#E69F00,stroke:#555555,color:#000 class EA entry class GW,MCP infra class ALLOW decision class CATALOG,T2D action ``` Requires `pydantic-ai-slim[fastmcp]` and `fastmcp`: ```python theme={null} from fastmcp import Client from fastmcp.client.transports import StreamableHttpTransport from pydantic_ai import Agent from pydantic_ai.toolsets.fastmcp import FastMCPToolset transport = StreamableHttpTransport( url="https://craft.emergence.ai/mcp", headers={ "Authorization": "Bearer ", "X-Project-ID": "", }, ) toolset = FastMCPToolset(Client(transport)) agent = Agent("anthropic:claude-opus-4-8", toolsets=[toolset]) async def main(): result = await agent.run( "What databases are available? Generate SQL to count active customers." ) print(result.output) ``` `MCPServerHTTP` is deprecated — use `FastMCPToolset` with `StreamableHttpTransport`. Requires `google-adk`: ```python theme={null} from google.adk.agents import LlmAgent from google.adk.tools.mcp_tool import MCPToolset, StreamableHTTPConnectionParams agent = LlmAgent( model="gemini-3.5-flash", tools=[ MCPToolset( connection_params=StreamableHTTPConnectionParams( url="https://craft.emergence.ai/mcp", headers={ "Authorization": "Bearer ", "X-Project-ID": "", }, ) ) ], ) ``` `SseServerParams` is the legacy SSE transport. Use `StreamableHTTPConnectionParams` for the MCP 2025-11-25 streamable-HTTP transport. Any HTTP client can speak the MCP JSON-RPC protocol. List available tools: ```bash theme={null} # 1. Initialize a session curl -s -X POST https://craft.emergence.ai/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer " \ -H "X-Project-ID: " \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}' \ | jq . ``` ```bash theme={null} # 2. List tools (include MCP-Session-Id from initialize response) curl -s -X POST https://craft.emergence.ai/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "X-Project-ID: " \ -H "MCP-Session-Id: " \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | jq '.result.tools[].name' ``` ```bash theme={null} # 3. Call a tool curl -s -X POST https://craft.emergence.ai/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -H "X-Project-ID: " \ -H "MCP-Session-Id: " \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_schema","arguments":{"query":"customer"}}}' \ | jq . ``` Include `MCP-Session-Id` on all requests after initialization for stateful sessions. Headless callers need a programmatic access token. Obtain one via a Keycloak service account (client credentials grant). See [Service Accounts](/platform/service-accounts) for setup. ## Available tools ### Catalog tools Always available — provide governed access to your project's data catalog. | Tool | Required parameters | Returns | | ---------------- | -------------------------------------------------------------------- | --------------------------------------------------------- | | `hello_world` | — | Server greeting; confirms connectivity (no auth required) | | `list_databases` | `resource_uri` — data connection UUID | Database metadata list | | `get_schema` | `resource_uri`, `fqn` — 2 segments = database, 3 = schema, 4 = table | Full entity schema including columns and types | | `search_schema` | `query` — Elasticsearch-style text query | Matching metadata entries across the project | ### Talk2Data tools Available when the deployment includes the Data Insights solution. See [Data Connections](/platform/data-connections) to connect your data sources. | Tool | Required parameters | Returns | | ----------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `generate_sql` | `question` (natural language), `database` (connection UUID), `schema` (object with `schema_name`) | `sql`, `explanation`, `assumptions` | | `execute_query` | `sql` (SELECT statements only), `database` | `columns`, `rows`, `row_count`, `truncated` | | `resolve_term` | `term` — business term (e.g., "ARR", "churn rate") | `definition`, `related_columns`, `sql_pattern` | | `generate_plotly_chart` | `data` (tabular), `chart_type` (bar, line, scatter, pie, …) | Plotly JSON figure spec (`plotly_json`) + interactive iframe for MCP Apps clients | | `sample_data` | `resource_uri`, `table_fqn` — `database.schema.table` | `columns`, `rows` (up to 100 rows preview) | `execute_query` only accepts SELECT statements — DDL, DML, and admin functions are rejected. `generate_sql` delegates to the Text2SQL A2A agent; the first call in a session may take 3–8 s (cold start), subsequent calls \~1–2 s. `generate_plotly_chart` emits per-stage MCP progress notifications (parse → validate → render → verify) forwarded as A2A status events — visible in clients that display progress. ## Per-project allowlist and governance CRAFT enforces a per-project tool allowlist — each project defines which tools each agent identity may call. This is the key difference between the CRAFT MCP gateway and a raw FastMCP server. Configure allowlists in [Platform → Authorization](/platform/authorization) (RBAC model) and [Platform → Agents](/platform/agents) (agent identity → allowlist mapping). Denied calls return a structured error: ```json theme={null} { "ok": false, "error": { "code": "tool_not_allowed", "message": "Tool 'generate_sql' is not in the allowlist for this agent identity" } } ``` Every tool call — allowed or denied — writes an audit event to the governance event log. Each event records: * Timestamp, agent identity, tool name, project ID * Input parameters (PII-scrubbed per governance policy) * Result status (allowed / denied / error) Query audit events via [Audit Log](/guides/customer-admin/audit-log). Tool calls are tracked separately from LLM completions and do not flow through the LiteLLM gateway. See [LLM Gateway](/platform/llm-gateway) for the complementary table. ## Errors and limits | Condition | HTTP | MCP error code | Remediation | | --------------------------------- | ---- | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | Missing `Authorization` header | 401 | `missing_authorization` | Complete the OAuth flow; verify token is fresh | | Missing `X-Project-ID` header | 400 | `missing_project` | Add `X-Project-ID` header with your project UUID | | Tool not in allowlist | 403 | `tool_not_allowed` | Ask project admin to add tool to allowlist | | Non-SELECT SQL in `execute_query` | 422 | `validation_error` | Only SELECT statements are permitted | | Text2SQL agent unavailable | 503 | `upstream_unavailable` | Retry; `generate_sql` depends on the Talk2Data A2A agent | | Query row limit hit | 200 | — | Response includes `"truncated": true`; add a `LIMIT` or more selective `WHERE` clause to fit within 500 rows | **Limits:** * `execute_query`: server-enforced cap of 500 rows; `truncated: true` in response when exceeded * `sample_data`: up to 100 rows (`limit` parameter; default 10) * `generate_sql`: first call may take 3–8 s (Text2SQL agent cold-start); subsequent calls \~1–2 s ## Next steps The complementary gateway for LLM completions — model allowlist, rate limits, and cost attribution. Connect your data sources to enable Talk2Data tools. Track new tools, breaking changes, and platform updates. # Context Packs Source: https://docs.emergence.ai/platform/memory-context-packs Context Packs are the storage containers for agent memories, organizing related memories into retrievable, structured units that agents query for relevant context. # Context Packs A **Context Pack** is the fundamental storage unit for agent memories. It groups related memories into a named, typed container that agents query when they need relevant background knowledge before responding or acting. ## Three-Layer Architecture Context Packs use a three-layer structure that balances retrieval speed with information depth: ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB Query["Agent Query"] --> S["Summary Layer
(fast, always loaded)"] S --> M["Metadata Layer
(selective, on match)"] M --> C["Content Layer
(full detail, on demand)"] classDef query fill:#56B4E9,stroke:#555555,color:#000 classDef layer fill:#E69F00,stroke:#555555,color:#000 class Query query class S,M,C layer ``` | Layer | Contents | When Loaded | | ------------ | ------------------------------------------------------------------- | ------------------------------------ | | **Summary** | High-level description, topic tags, last-updated timestamp | Always, fast pack discovery | | **Metadata** | Memory count, type distribution, confidence scores, access patterns | On pack selection | | **Content** | Full memory records with embeddings, provenance, relationships | On demand, specific memory retrieval | ## Two-Phase Query Model Agents retrieve context through a two-phase process that avoids loading unnecessary detail: Query Context Pack summaries to find relevant packs. Returns pack IDs, topic descriptions, and match scores. Fast, only summary layer is read. For matched packs, retrieve specific memories using semantic search within the pack's content layer. Returns ranked memory records with scores. ## Context Pack Types | Type | Purpose | Decay rate | | ---------------- | ------------------------------------------------------------- | -------------------------- | | **session** | Single conversation context, ephemeral, high detail | Aggressive (hours to days) | | **user** | Long-term user preferences and history, persistent | Conservative (months) | | **domain** | Domain knowledge, facts, rules, entity relationships | Very conservative (years) | | **project** | Project-specific context, decisions, conventions, constraints | Conservative (months) | | **team** | Shared team knowledge and conventions | Conservative (months) | | **data\_source** | Data-source-specific schema, joins, and profiling context | Conservative (months) | ## Memory Record Schema Each memory stored in a Context Pack has the following structure: ```json theme={null} { "id": "uuid", "content": "The main memory text (self-contained, context-independent)", "memory_type": "fact | experience | observation | instruction | preference | summary | glossary | ontology | textual_pattern | kpi | exemplar | join | numeric_pattern | policy", "name": "optional_label_for_direct_retrieval", "metadata": { "source": "conversation | document | agent_message", "confidence": 0.92, "created_at": "2026-04-07T10:30:00Z", "last_accessed": "2026-04-07T14:22:00Z", "access_count": 7, "superseded_by": null, "provenance": "Session #142, turn 3" }, "embedding": [0.123, -0.456, ...], "relationships": [ {"type": "supersedes", "target_id": "uuid-of-old-memory"}, {"type": "related_to", "target_id": "uuid-of-related-memory"} ], "degradation_tier": "full | summary | key_facts | existence_marker | archived" } ``` ## Managing Context Packs via API ### List Packs ```python theme={null} import httpx response = httpx.get( "https://api.example.com/utils/context-packs", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, params={"pack_type": "domain"} ) packs = response.json()["data"] ``` ### Create a Pack ```python theme={null} response = httpx.post( "https://api.example.com/utils/context-packs", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, json={ "name": "semiconductor_domain", "pack_type": "domain", "description": "Knowledge about semiconductor fabrication processes and terminology", "meta_data": {"tags": ["semiconductor", "fabrication", "yield"]} } ) ``` ### Query a Pack (Phase 2 retrieval) ```python theme={null} response = httpx.get( f"https://api.example.com/utils/context-packs/{pack_name}/memories", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, params={ "memory_type": "fact", "page_size": 5 } ) memories = response.json()["data"] ``` ## Degradation Tiers Memories in Context Packs degrade gracefully over time when not accessed: | Tier | Description | Storage impact | | --------------------- | -------------------------------------- | -------------- | | **full** | Complete memory record with all detail | Full | | **summary** | Key points only, detail compressed | \~30% | | **key\_facts** | Core facts only, context removed | \~10% | | **existence\_marker** | Memory existed, no content | Minimal | | **archived** | Moved to cold storage, not retrieved | Off-heap | Any retrieval access re-promotes a memory to the **full** tier. This ensures frequently-used memories remain fully detailed while inactive memories are progressively compressed. ## Related Overview of the memory system and API usage. Step-by-step guide to adding memory to your agents. # Memory Service Source: https://docs.emergence.ai/platform/memory-service Multi-agent memory management for CRAFT, how agents remember and learn from interactions using Context Packs. # Memory Service The Memory Service provides multi-agent memory management for CRAFT. It gives agents the ability to remember information across conversations, accumulate domain knowledge, and personalize responses based on prior interactions. Memory is built into the **Utils service** (`em-runtime-utils`). No separate service is required — the same Utils API that handles schedules, metadata, and catalog tags also manages Context Packs and Memories. ## Key Concepts ### Memories A **memory** is a discrete, self-contained piece of information extracted from conversations, documents, or agent messages. Each memory has: * **Content**: The information itself (a fact, preference, insight, or relationship) * **Type**: The category of memory (`fact`, `preference`, `observation`, `glossary`, `policy`, etc., see [Memory Types](#memory-types) below) * **Name**: An optional label for direct retrieval * **Metadata**: Source, timestamp, confidence, access count * **Embeddings**: Vector representation for semantic search ### Context Packs A **Context Pack** is the storage container for a related set of memories. Context Packs organize memories by domain or purpose and provide efficient retrieval. See [Context Packs](/platform/memory-context-packs) for the detailed architecture. ### Memory Lifecycle ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% stateDiagram-v2 [*] --> Active : extracted + worthiness score passes Active --> Summarized : inactivity decay Summarized --> KeyFacts : further decay KeyFacts --> ExistenceMarker : further decay ExistenceMarker --> Archived : maximum decay Archived --> [*] : purged after retention period Summarized --> Active : retrieved (re-promotion) KeyFacts --> Active : retrieved (re-promotion) ExistenceMarker --> Active : retrieved (re-promotion) Archived --> Active : explicitly re-promoted ``` Memories move through a managed lifecycle: 1. **Created**, extracted from input and scored for worthiness 2. **Active**, retrievable and contributing to agent context 3. **Degraded**, fading through summarization tiers (full → key facts → existence marker) 4. **Archived**, removed from active retrieval but preserved for audit 5. **Re-promoted**, any retrieval restores a degraded memory to active state ## Memory Types Memories are categorized into two groups: ### General Types | Type | Purpose | | ------------- | --------------------------------------------------------- | | `fact` | Factual information about entities, processes, or systems | | `experience` | Learned behaviors from past interactions | | `observation` | Observed patterns or anomalies | | `instruction` | Directives or guidelines for agent behavior | | `preference` | User or agent preferences (e.g., preferred output format) | | `summary` | Condensed summaries of conversations or data | ### Data Governance Enrichment Types | Type | Purpose | | ----------------- | ------------------------------------------------- | | `glossary` | Business glossary terms and definitions | | `ontology` | Domain ontology relationships and classifications | | `textual_pattern` | Regex or text patterns identified in data columns | | `kpi` | Key performance indicator definitions | | `exemplar` | Representative data examples for reference | | `join` | Table join relationships and foreign key mappings | | `numeric_pattern` | Numeric distribution patterns in data columns | | `policy` | Data governance policies and rules | ## Using the Memory API All memory operations go through the Utils service (`/api/utils/`). Authentication uses the same JWT tokens as all other platform APIs, plus an `X-Project-ID` header to scope results to the current project. ### Add a Memory to a Context Pack Memories are created within a named Context Pack: ```python theme={null} import httpx response = httpx.post( f"https://api.example.com/utils/context-packs/{pack_name}/memories", headers={ "Authorization": f"Bearer {token}", "X-Project-ID": project_id, }, json={ "name": "sql_explanation_style", "content": { "type": "text", "text": "The user prefers detailed SQL explanations with comments", }, "memory_type": "preference", "source": {"type": "conversation"}, }, ) memory = response.json() print(memory["id"]) # UUID of the created memory ``` ### List Memories ```python theme={null} # All memories in the project (across all packs) response = httpx.get( "https://api.example.com/utils/memories", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) all_memories = response.json() # {"data": [...], "pagination": {...}} # Memories within a specific Context Pack response = httpx.get( f"https://api.example.com/utils/context-packs/{pack_name}/memories", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) pack_memories = response.json() ``` ### Retrieve by ID ```python theme={null} response = httpx.get( f"https://api.example.com/utils/memories/{memory_id}", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) memory = response.json() ``` ### Retrieve by Name within a Pack ```python theme={null} response = httpx.get( f"https://api.example.com/utils/context-packs/{pack_name}/memories/{memory_name}", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) memory = response.json() ``` ### Update a Memory ```python theme={null} response = httpx.patch( f"https://api.example.com/utils/memories/{memory_id}", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, json={ "content": { "type": "text", "text": "The user prefers concise SQL explanations with minimal comments", }, }, ) ``` ### Delete a Memory ```python theme={null} # Soft delete (moves to deleted state, recoverable) response = httpx.delete( f"https://api.example.com/utils/memories/{memory_id}", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) # Returns 200 OK with a MemoryDeleteResponse body confirming the soft-delete. # Permanent delete (irreversible) response = httpx.post( f"https://api.example.com/utils/memories/{memory_id}/permanent-delete", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) ``` ### Lifecycle Operations ```python theme={null} # Pin a memory — keeps it in the Active tier, bypasses inactivity decay httpx.post( f"https://api.example.com/utils/memories/{memory_id}/pin", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) # Archive a memory — moves it to Archived state httpx.post( f"https://api.example.com/utils/memories/{memory_id}/archive", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) # Supersede — mark old_memory_id as superseded by new_memory_id httpx.post( f"https://api.example.com/utils/memories/{old_memory_id}/supersede/{new_memory_id}", headers={"Authorization": f"Bearer {token}", "X-Project-ID": project_id}, ) ``` ## Scoping Memories are scoped per organization and project: * **Organization**: determined automatically from the JWT's `org_id` claim — no separate header needed * **Project**: pass the `X-Project-ID` header on every request ```http theme={null} Authorization: Bearer X-Project-ID: ``` ## Related Deep dive into the Context Pack storage architecture. How-to guide for integrating memory into your agents. Full memory endpoint API reference. # Multi-Tenancy Source: https://docs.emergence.ai/platform/multi-tenancy Tenant isolation architecture with Keycloak realm separation, org_id scoping, dual authorization, and per-service database partitioning. # Multi-Tenancy CRAFT implements multi-tenancy at every layer of the stack. Each organization is fully isolated through a combination of **identity separation** (Keycloak realms), **authorization scoping** (OpenFGA relationship tuples), and **data partitioning** (org\_id filtering on every query). This page describes the architecture that makes this work. ## Isolation Layers ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TB REQ["Inbound Request\n(user or service account)"] KC["Keycloak Realm\n(identity isolation)"] JWT["JWT Validation\n(org_id extracted)"] OF["OpenFGA\n(authorization tuples)"] FILT["org_id Filter\n(data isolation)"] DB[("Per-Service DB\n(service isolation)")] REQ --> KC KC -->|"issues JWT\nwith org_id claim"| JWT JWT --> OF JWT --> FILT OF -->|"permission check\n(resource-scoped)"| FILT FILT --> DB classDef entry fill:#56B4E9,stroke:#555555,color:#000 classDef identity fill:#56B4E9,stroke:#555555,color:#000 classDef authz fill:#E69F00,stroke:#555555,color:#000 classDef data fill:#009E73,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff class REQ entry class KC,JWT identity class OF authz class FILT data class DB infra ``` The platform enforces tenant isolation through four complementary mechanisms: Each organization is a separate Keycloak realm. Users, groups, sessions, and identity provider configurations are completely isolated between realms. A user in `acme-corp` cannot see or interact with users in `globex`. OpenFGA relationship tuples are scoped to specific resources. Permission checks always include the resource ID, ensuring a user in one organization cannot access resources in another even if they somehow obtain a valid token. Every database query includes an `organization_id` filter derived from the JWT token. This is enforced at the application layer -- queries without `org_id` filters are treated as security vulnerabilities. Each platform service (Governance, Assets, Utils) owns its own PostgreSQL database. There are no cross-service foreign keys, preventing accidental data leakage through join queries. ## How org\_id Flows Through the System The organization ID is the foundation of tenant isolation. It flows through every request: The user authenticates against their organization's Keycloak realm. The JWT token's `iss` (issuer) claim contains the realm name, which becomes the `org_id`. ```text theme={null} iss: "https://keycloak.example.com/realms/acme-corp" ^^^^^^^^^ org_id ``` The Governance service validates the JWT using the realm's JWKS public keys. The `org_id` is extracted from the verified issuer claim and stored in the `Auth` object. Users in the `master` realm (platform developers) have `org_id = null`. Every API endpoint receives the `Auth` object via dependency injection. The `org_id` is used to: * **Filter list queries** to return only the organization's resources * **Scope write operations** to associate new resources with the correct organization * **Validate access** via OpenFGA permission checks When Assets or Utils need to check permissions, they forward the user's JWT token to the Governance service via auto-generated SDK calls. The Governance service re-validates the token and checks OpenFGA -- the `org_id` is never passed as a parameter that could be spoofed. ## Dual Authorization Pattern The platform uses a dual authorization pattern that combines database-level filtering with authorization checks: List endpoints apply **both** database filtering and permission checks: ```python theme={null} @router.get("/data") async def list_data_connections( ctx: RequestContextDep, # Permission check: user can read resources in this project _: None = Depends(require_permission( Permissions.PROJECT, Permissions.PROJECT.actions.CAN_READ, HEADER_PROJECT_ID )) ): # Database filter: always scope by org_id from JWT connections = await service.list_data_connections( organization_id=ctx.org_id, # From JWT, never from request body project_id=ctx.project_id, # From X-Project-ID header ) ``` This defense-in-depth approach means that even if a permission check has a bug, the database query will not return another organization's data. Single-resource endpoints rely on **OpenFGA permission checks only**: ```python theme={null} @router.get("/data/{resource_uri}") async def get_data_connection( resource_uri: str, # Permission check: user can read this specific resource _: None = Depends(require_permission( Permissions.DATA_CONNECTION, Permissions.DATA_CONNECTION.actions.CAN_READ, "resource_uri" )) ): # OpenFGA already verified the user has access connection = await service.get_data_connection( organization_id=ctx.org_id, resource_uri=resource_uri, ) ``` Even here, the `organization_id` filter is included as an additional safety measure. **Security rules enforced across all services:** * Never trust user-supplied `org_id` from request body or parameters. Always use `auth.org_id` from the JWT. * Never list resources without an `org_id` filter. Omitting the filter returns all organizations' data. * Always use `auth.org_id` when creating new resources. This ensures the resource is correctly scoped to the authenticated user's organization. ## Database Architecture Each service owns an independent PostgreSQL database with no cross-service foreign keys: ```text theme={null} PostgreSQL Server keycloak_db -- Keycloak identity data openfga_db -- OpenFGA authorization tuples infisical_db -- Infisical secrets data (present by default; unused if using ESO backend) governance_db -- Organizations, projects assets_db -- Artifacts, data connections, files, models utils_db -- Data catalog, scheduling, context packs, memories ``` Every resource table includes an `organization_id` column: | Column | Type | Purpose | | ----------------- | ------------- | -------------------------------------------------- | | `organization_id` | `VARCHAR` | Tenant partition key, always populated from JWT | | `project_id` | `VARCHAR` | Project scope within the organization | | `created_at` | `TIMESTAMPTZ` | Record creation timestamp (via `TimestampMixin`) | | `updated_at` | `TIMESTAMPTZ` | Last modification timestamp (via `TimestampMixin`) | Each service runs its own Alembic migrations independently. There are no cross-service migration dependencies, which means services can be upgraded independently without coordinating database schema changes. ## Keycloak Realm Structure When an organization is created, the Governance service provisions a complete Keycloak realm: ```text theme={null} acme-corp (realm) Groups: org-owners -- Organization owners org-admins -- Organization administrators org-members -- Standard members project-owners -- Project-level owners project-admins -- Project-level administrators project-developers -- Project-level developers project-operators -- Project-level operators project-viewers -- Project-level viewers Mappers: groups-mapper -- Ensures group memberships appear in JWT "groups" claim Clients: em-runtime-ui -- Frontend application client (PKCE) Identity Providers: (configurable per realm: SAML, OIDC, LDAP, social login) ``` ## Service Account Cross-Tenant Access Service accounts (background workers, automated processes) authenticate via the `master` realm and specify the target organization via headers: | Header | Purpose | | ---------------- | ------------------------------------- | | `X-Org-Id` | Target organization for the operation | | `X-On-Behalf-Of` | Original user ID for audit trails | These headers are only trusted when the caller passes all three service account checks: 1. Token issued by `master` realm 2. Client ID starts with `svc-` 3. `serviceAccount` role in `realm_access.roles` Regular user tokens cannot use these headers -- the platform silently ignores them for non-service-account callers. ## Service Startup and Dependencies The multi-tenant infrastructure requires a specific startup order: ```text theme={null} PostgreSQL -> Redis -> Keycloak -> OpenFGA -> [Secrets backend] -> Governance -> Assets/Utils ``` The Governance service must be running before Assets or Utils can validate permissions. If Governance is down, downstream services return 403 for all requests because permission checks fail. `make docker-run` handles the startup order automatically via Docker Compose health checks. For manual local development, start `make run-deps` first, wait for readiness, then start Governance before Assets or Utils. ## Next Steps Learn how organizations are provisioned with Keycloak realms. Understand the multi-realm JWT authentication flow. Explore OpenFGA permission inheritance across tenants. See how projects provide the second level of scoping within tenants. # Organizations Source: https://docs.emergence.ai/platform/organizations Multi-tenant organization management with Keycloak realm isolation, default group provisioning, and OpenFGA permission bootstrapping. # Organizations Organizations are the top-level tenant boundary in CRAFT. Each organization maps 1:1 to a **Keycloak realm**, providing full identity isolation between tenants. When you create an organization, the platform provisions a complete identity and authorization environment automatically. Every resource in the platform is scoped to an organization via `organization_id`. The org ID is never supplied by the client in request bodies -- it is always extracted from the JWT token's issuer claim, which identifies the Keycloak realm. ## How Organizations Work ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% graph TB API["POST /organizations"] subgraph KC["Keycloak"] REALM["New Realm\n(org_id as realm name)"] GRP["Default Groups\norg-owners / org-admins / org-members"] MAP["Groups Mapper\n(claims in JWT)"] end subgraph OF["OpenFGA"] TUPLES["Permission Tuples\nowner / admin / member relations"] end DB[("Governance DB\norg record")] JWT["JWT tokens carry\norg_id from realm issuer"] API --> REALM REALM --> GRP --> MAP GRP --> TUPLES API --> DB MAP --> JWT classDef api fill:#E69F00,stroke:#555555,color:#000 classDef kc fill:#56B4E9,stroke:#555555,color:#000 classDef authz fill:#CC79A7,stroke:#555555,color:#000 classDef db fill:#009E73,stroke:#555555,color:#000 classDef token fill:#F0E442,stroke:#555555,color:#000 class API api class REALM,GRP,MAP kc class TUPLES authz class DB db class JWT token ``` Creating an organization triggers a coordinated setup across three systems: A new Keycloak realm is created using the organization ID as the realm name. This realm holds all users, groups, and identity provider configurations for the tenant. Default groups are created within the realm to establish the role hierarchy: * `org-owners` -- Full organization control * `org-admins` -- Administrative access (user and project management) * `org-members` -- Standard membership with read access A groups mapper is configured so group memberships appear as claims in JWT tokens. Permission tuples are written to OpenFGA mapping each group to its corresponding relation: * `org-owners` -> `owner` relation on the organization * `org-admins` -> `admin` relation on the organization * `org-members` -> `member` relation on the organization This means any user added to `org-admins` in Keycloak automatically inherits `admin` permissions across the platform. An organization record is created in the Governance database with the org ID, name, description, and timestamps. ## Organization ID in JWT The organization ID is derived from the Keycloak realm embedded in the token's `iss` (issuer) claim. When a user authenticates against a realm, the platform extracts the realm name as the `org_id`: ```text theme={null} Token issuer: https://keycloak.example.com/realms/acme-corp ^^^^^^^^^ org_id = "acme-corp" ``` This design means: * The `org_id` is **cryptographically bound** to the token (it comes from the issuer verified by JWKS signature) * Clients cannot forge or override the organization context * Users in the `master` realm (platform developers) have `org_id = null` ## API Reference The Organizations API is part of the Governance service (port 8000). Creates a new organization with Keycloak realm, default groups, and OpenFGA permissions. **Access:** Platform developers only (master realm) ```json Request Body theme={null} { "id": "acme-corp", "name": "Acme Corporation", "description": "Production tenant for Acme Corp", "create_users": true } ``` The `id` must follow Keycloak realm naming rules: alphanumeric characters, hyphens, and underscores only. When `create_users` is `true`, default users are created and added to the appropriate groups for immediate access. ```json Response (201 Created) theme={null} { "id": "acme-corp", "name": "Acme Corporation", "description": "Production tenant for Acme Corp", "created_at": "2026-04-01T12:00:00Z" } ``` Retrieves organization details by ID. **Access:** Users with `can_read` permission on the organization (owners, admins, or members). ```json Response (200 OK) theme={null} { "id": "acme-corp", "name": "Acme Corporation", "description": "Production tenant for Acme Corp", "created_at": "2026-04-01T12:00:00Z", "updated_at": "2026-04-02T08:30:00Z" } ``` Deletes an organization and cascades to all its projects. Removes OpenFGA permission tuples, the Keycloak realm (including all users and groups), and database records. **Access:** Platform developers only (master realm). Idempotent: returns 204 whether the organization was deleted or was already absent. ## Bootstrap Organization During first startup, the Governance service automatically bootstraps a default organization: ```yaml theme={null} bootstrap: organization: id: "emergence" name: "Emergence" description: "Emergence organization" create_admin_user: true ``` When `create_admin_user` is `true` (the default), a user named `emergence` is created with: * **Username:** `emergence` * **Password:** `emergence` (change immediately in production) * **Group:** `org-admins` * **OpenFGA relation:** `admin` on the organization The bootstrap admin user is intended for development and initial setup. Always change the default password in production environments. ## Organization Permissions Organizations define the following computed permissions in the OpenFGA schema: | Permission | Owner | Admin | Member | | --------------------- | :---: | :---: | :----: | | `can_read` | Yes | Yes | Yes | | `can_write` | Yes | -- | -- | | `can_delete` | Yes | -- | -- | | `can_manage_projects` | Yes | Yes | -- | | `can_manage_users` | Yes | Yes | -- | | `can_read_secrets` | Yes | Yes | Yes | | `can_manage_secrets` | Yes | Yes | -- | | `can_read_metadata` | Yes | Yes | Yes | | `can_manage_metadata` | Yes | Yes | -- | ## Next Steps Learn how projects scope resources within organizations. Understand the full tenant isolation architecture. See how Keycloak realms power the authentication flow. Explore the OpenFGA permission model in depth. # CRAFT Platform Overview Source: https://docs.emergence.ai/platform/overview The shared platform layer providing governance, asset management, and utilities with built-in identity, authorization, and secrets management. # Platform Overview The platform layer gives your solutions shared infrastructure for identity, authorization, resource management, and a unified UI shell. It includes three core services (Governance, Assets, Utilities), a shared common library, and auto-generated SDKs. The platform is the foundation that every solution depends on. Solutions never implement their own authentication, authorization, or resource management -- they delegate to the platform via SDKs. ## Authentication Flow Authentication uses **Keycloak** with a multi-realm architecture where each organization is a separate realm: The user authenticates via OIDC/PKCE flow against their organization's Keycloak realm. The browser receives a JWT token. All API requests include the JWT in the `Authorization` header. The `X-Project-ID` header specifies the active project context. The Governance service validates the JWT, extracts `org_id` from the realm, and checks OpenFGA for the requested permission. If authorized, the request proceeds. If not, a 403 response is returned. Assets and Utils delegate all permission checks to Governance via SDK calls. ## Authorization Model Authorization uses **OpenFGA** for Relationship-Based Access Control (ReBAC): | Concept | Description | | ------------------------ | -------------------------------------------------------------------------- | | **Roles** | `owner`, `admin`, `member`, `developer`, `operator`, `viewer` | | **Computed permissions** | `can_read`, `can_write`, `can_delete`, `can_execute`, `can_manage_secrets` | | **Inheritance** | Projects inherit from organizations; resources inherit from projects | | **Schema** | Defined in `openfga-schema.fga` (DSL) and `openfga-schema.json` (runtime) | When modifying the OpenFGA schema, always update both the `.fga` DSL file and regenerate the `.json` runtime file. The Governance service reads only the JSON file at startup. Adding new permissions is safe; renaming or removing is a breaking change. ## Next Steps Learn how organizations provide multi-tenant isolation. Explore the unified registry for A2A agents, MCP servers, and skills. Deep dive into Keycloak multi-realm authentication. Understand OpenFGA and the ReBAC permission model. Build a new solution on top of these services — start with the 30-minute quickstart. # Projects Source: https://docs.emergence.ai/platform/projects Logical resource groupings within organizations, providing project-level scoping, permission inheritance, and the X-Project-ID header convention. # Projects Projects are the second level of resource scoping in CRAFT. Every resource (agents, data connections, artifacts, files) belongs to a project, and every project belongs to an organization. Projects provide: * **Logical separation** of resources within a single organization * **Permission inheritance** from the parent organization via OpenFGA * **Access control** through project-level roles (`owner`, `admin`, `developer`, `operator`, `viewer`) The `project_id` is never embedded in the JWT token. It is supplied via the `X-Project-ID` request header, allowing users to switch project context without re-authenticating. ## How Projects Work When a project is created, the platform performs several coordinated actions: A project record is created in the Governance database, linked to the parent organization by `organization_id`. A relationship tuple is written to OpenFGA linking the project to its organization: `project:{project_id}#organization@organization:{org_id}`. This establishes permission inheritance -- organization-level roles automatically grant access to the project. Default project groups receive appropriate permissions: * `project-owners` -> `owner` relation * `project-admins` -> `admin` relation * `project-developers` -> `developer` relation * `project-operators` -> `operator` relation * `project-viewers` -> `viewer` relation ## The X-Project-ID Header All resource APIs (Assets, Utils) require the `X-Project-ID` header to scope requests: ```bash theme={null} # List data connections in a specific project curl -H "Authorization: Bearer $TOKEN" \ -H "X-Project-ID: my-analytics-project" \ https://api.example.com/assets/data ``` This design choice has important implications: Users often work across multiple projects in the same session. Embedding `project_id` in the JWT would require re-authentication to switch projects. The header-based approach allows instant project switching with the same token. The `require_permission()` dependency reads the `X-Project-ID` header and checks OpenFGA to verify the authenticated user has the required permission on that project. If the user lacks access, a 403 is returned. List queries always filter by both `auth.org_id` (from JWT) and `project_id` (from header). This dual filter ensures data isolation even if a permission check has a gap. ## API Reference The Projects API is part of the Governance service (port 8000). Lists all projects in the user's organization with pagination. Results are filtered to only include projects the user has `can_read` permission on. **Query Parameters:** * `page` (default: 1) -- Page number * `limit` (default: 20, max: 100) -- Items per page ```json Response (200 OK) theme={null} { "data": [ { "id": "analytics-prod", "name": "Analytics Production", "description": "Production analytics project", "organization_id": "acme-corp", "created_at": "2026-04-01T12:00:00Z" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "total_pages": 1 } } ``` Creates a new project within the user's organization (determined from JWT). The project is automatically linked to the organization and default groups receive permissions. **Access:** Users with `can_manage_projects` on the organization (owners or admins). ```json Request Body theme={null} { "name": "Analytics Production", "description": "Production analytics project", "external_id": "analytics-prod", "create_users": false } ``` If `external_id` is omitted, a UUID is generated for the project ID. ```json Response (201 Created) theme={null} { "id": "analytics-prod", "external_id": "analytics-prod", "name": "Analytics Production", "organization_id": "acme-corp", "created_at": "2026-04-01T12:00:00Z" } ``` Retrieves detailed information about a specific project. **Access:** Users with `can_read` permission on the project. ```json Response (200 OK) theme={null} { "name": "Analytics Production", "description": "Production analytics project", "organization_id": "acme-corp", "created_at": "2026-04-01T12:00:00Z", "updated_at": "2026-04-02T08:30:00Z" } ``` ## Project Permissions Projects define a richer role set than organizations, supporting fine-grained access control for resource management: | Permission | Owner | Admin | Developer | Operator | Viewer | | ---------------------- | :---: | :---: | :-------: | :------: | :----: | | `can_read` | Yes | Yes | Yes | Yes | Yes | | `can_write` | Yes | Yes | Yes | Yes | -- | | `can_delete` | Yes | Yes | -- | -- | -- | | `can_create_resources` | Yes | Yes | Yes | -- | -- | | `can_read_secrets` | Yes | Yes | Yes | Yes | -- | | `can_manage_secrets` | Yes | Yes | Yes | -- | -- | | `can_read_metadata` | Yes | Yes | Yes | Yes | -- | | `can_manage_metadata` | Yes | Yes | Yes | -- | -- | Project permissions also inherit from the parent organization. An organization `owner` or `admin` automatically has `can_read`, `can_write`, `can_delete`, and `can_create_resources` on all projects in their organization, even without explicit project-level role assignment. ## Service Accounts Projects support **service relations** for background workers and automated processes: | Service Relation | Grants | | ------------------ | ------------------------------------------------------------------------------------------------------------- | | `service_reader` | `can_read`, `can_read_secrets`, `can_read_metadata` | | `service_writer` | `can_read`, `can_write`, `can_read_secrets`, `can_manage_secrets`, `can_read_metadata`, `can_manage_metadata` | | `service_deleter` | `can_read`, `can_delete` | | `service_executor` | `can_execute` (for MCP servers, API servers, data connections) | Service accounts authenticate via the master realm with a `svc-` client ID prefix and are granted contextual tuples scoped to specific projects. See [Service Accounts](/platform/service-accounts) for setup instructions and token management. ## Next Steps Understand how organizations provide the top-level tenant boundary. Explore the OpenFGA permission model and permission inheritance. Manage database and storage connections within projects. Register and discover agents scoped to projects. # Schedules Source: https://docs.emergence.ai/platform/schedules Scheduled task execution with cron patterns, timezone support, one-off scheduling, pg_cron integration, and run history tracking. # Schedules The scheduling system enables time-based automation on CRAFT. Schedules are managed by the **Utils** service (port 8000) and support both recurring cron-based schedules and one-off timed executions. When a schedule triggers, it publishes a notification to a configured topic, which downstream systems (such as workflow orchestrators) can consume. Schedules use **pg\_cron** for reliable execution. Cron jobs run inside PostgreSQL, ensuring schedule triggers survive application restarts and are executed exactly by the database scheduler. ## Schedule Types Recurring schedules defined using standard cron expressions with timezone support. ```json theme={null} { "type": "cron", "cron": "0 9 * * 1-5", "timezone": "America/New_York" } ``` The cron expression above triggers at 9:00 AM Eastern Time, Monday through Friday. Internally, the expression is converted to UTC for storage and execution, then converted back to the original timezone for display. **Cron expression format:** ``` ┌───────── minute (0-59) │ ┌─────── hour (0-23) │ │ ┌───── day of month (1-31) │ │ │ ┌─── month (1-12) │ │ │ │ ┌─ day of week (0-7, 0 and 7 are Sunday) │ │ │ │ │ * * * * * ``` Single-execution schedules that trigger at a specific date and time. ```json theme={null} { "type": "one_off", "at": "2026-06-15T14:30:00", "timezone": "Europe/London" } ``` The datetime must be: * **Timezone-naive** (no `Z` or offset suffix) -- the `timezone` field specifies interpretation * **Minute-precision** -- seconds and microseconds must be zero (pg\_cron limitation) ## Notification Configuration When a schedule triggers, it publishes a notification. Currently, the `NOTIFICATION` config type is supported: ```json theme={null} { "type": "NOTIFICATION", "topic": "data-readiness.workflow.run", "extra_data": { "workflow_config_id": "wf-abc123", "environment": "production" } } ``` | Field | Description | | ------------ | ---------------------------------------------------------------------- | | `topic` | Topic string for the notification (consumed by workflow orchestrators) | | `extra_data` | Optional JSON payload included in the notification | ## Schedule Lifecycle Create a schedule with a name, schedule configuration (cron or one-off), a reference ID linking to the target resource, and a notification config. Schedules can be created in an enabled or disabled state. Enabling a schedule registers it with pg\_cron. Disabling unschedules it. Both operations are idempotent. When pg\_cron fires, a schedule run record is created with a snapshot of the schedule configuration at execution time. The notification is published to the configured topic. Query the runs API to view execution history, including status (PENDING, SUCCESS, FAILURE) and failure reasons. ## API Reference All endpoints are scoped to the authenticated user's organization. The `X-Project-ID` header optionally filters schedules by project. Creates a new schedule. **Access:** `can_manage_projects` on the organization. ```json Request Body theme={null} { "schedule_name": "Daily Analytics Refresh", "schedule": { "type": "cron", "cron": "0 6 * * *", "timezone": "America/Chicago" }, "reference_id": "wf-config-analytics-daily", "config": { "type": "NOTIFICATION", "topic": "data-readiness.workflow.run", "extra_data": { "workflow_config_id": "wf-config-analytics-daily" } }, "enabled": true } ``` ```json Response (201 Created) theme={null} { "schedule_id": "sch-a1b2c3d4" } ``` Lists all schedules in the organization. Optionally filter by project via the `X-Project-ID` header. Schedule values are returned in their original timezone for user-friendly display. Retrieves a single schedule by ID. Must belong to the authenticated user's organization. Full update of a schedule. All fields are required (this is not a partial update). Enables a schedule and registers it with pg\_cron. Idempotent if already enabled. Disables a schedule and unschedules it from pg\_cron. Idempotent if already disabled. Permanently deletes a schedule. This action cannot be undone. ### Schedule Runs Lists schedule runs with pagination. **Query Parameters:** * `page`, `limit` -- Pagination (default: page 1, 20 per page) * `order` -- Sort by `created_at`: `desc` (most recent first) or `asc` * `schedule_id` -- Filter runs for a specific schedule ```json Response theme={null} { "data": [ { "id": "run-x1y2z3", "schedule_id": "sch-a1b2c3d4", "organization_id": "acme-corp", "project_id": "analytics-prod", "status": "SUCCESS", "failure_reason": null, "created_at": "2026-04-03T06:00:00Z" } ], "pagination": { "page": 1, "limit": 20, "total": 42, "total_pages": 3 } } ``` Retrieves a single run with its full schedule snapshot -- the exact schedule configuration at the time of execution. Deletes a single schedule run record. ## Timezone Handling Timezone support is a first-class feature of the scheduling system: All cron expressions and one-off datetimes are converted to **UTC** before storage. The original timezone name is preserved alongside the expression. API responses dynamically convert UTC values back to the original timezone. A cron expression stored as `0 11 * * *` (UTC) for a schedule created with timezone `America/New_York` is returned as `0 6 * * *` (EST) or `0 7 * * *` (EDT) depending on the current DST status. Timezone values are validated against the IANA timezone database (e.g., `America/New_York`, `Europe/London`, `Asia/Tokyo`). Invalid timezone names are rejected with a descriptive error. Always use IANA timezone names (e.g., `America/New_York`) rather than fixed offsets (e.g., `UTC-5`). IANA names automatically handle daylight saving time transitions. ## Next Steps Trigger actions via event-driven webhooks. Connect schedules to data source workflows. Schedule automated agent tasks. Understand how schedules are scoped to projects. # Service Accounts Source: https://docs.emergence.ai/platform/service-accounts Service accounts enable background workers and automated processes to authenticate with CRAFT without user interaction. # Service Accounts Service accounts allow non-interactive processes, background workers, schedulers, cleanup jobs, and automated pipelines, to authenticate with CRAFT using machine identity. Service accounts authenticate against the **master realm** in Keycloak (not the organization realm), which gives them platform-level access scoped by explicit organization and project headers. ## Detection The platform identifies a request as coming from a service account when: 1. The client ID in the JWT begins with the `svc-` prefix (e.g., `svc-data-pipeline`) 2. The JWT contains the `serviceAccount` realm role 3. The authentication was performed against the Keycloak master realm Non-interactive processes using regular user tokens are rejected at service account-protected endpoints. ## Authentication Flow Service accounts use the OAuth 2.0 Client Credentials grant, no user interaction required. ```bash theme={null} curl -X POST \ "https://api.example.com/keycloak/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=svc-my-worker" \ -d "client_secret=" ``` Response: ```json theme={null} { "access_token": "", "token_type": "Bearer", "expires_in": 3600 } ``` Service account JWTs do not carry tenant context. Pass these required headers: ```http theme={null} Authorization: Bearer X-Org-Id: org_abc123 X-Project-ID: proj_xyz789 ``` * `X-Org-Id`, the organization context (required; replaces the realm-derived org in user tokens) * `X-Project-ID`, the project scope for project-scoped endpoints (required when applicable) To attribute the request to a specific user in audit logs, also include `X-On-Behalf-Of: ` (optional). The `X-On-Behalf-Of` header is recorded in audit logs but does not grant additional permissions. Include it whenever the service account is operating on a user's behalf. The `get_headers()` helper accepts it as an optional parameter. ## Creating a Service Account In the Keycloak master realm admin console: 1. Navigate to **Clients → Create client** 2. Set **Client ID** with the `svc-` prefix (e.g., `svc-nightly-cleanup`) 3. Set **Client Protocol**: `openid-connect` 4. Set **Access Type**: `confidential` 5. Enable **Service Accounts Enabled** 6. Save and note the generated client secret In the client's **Service Account Roles** tab: 1. Select **Realm Roles** 2. Assign the `serviceAccount` role This role is required for the platform to recognize the client as a service account. Service accounts access the platform APIs with the same permission model as users. Grant the service account access to specific organizations and projects via the platform admin API or the Runtime UI. ## Use Cases | Use Case | Example | | ------------------------------- | ------------------------------------------------------------------- | | **Scheduled data pipelines** | Nightly data ingestion jobs that create agents and data connections | | **Background cleanup** | Purging expired sessions, rotating secrets, archiving old artifacts | | **Workflow orchestration** | Prefect workflows that call platform APIs to register results | | **Automated testing** | CI/CD pipelines that create isolated test resources per run | | **Inter-service communication** | Solution services calling platform APIs | ## Token Expiry and Rotation Service account tokens are short-lived (configurable, default 1 hour). Background workers should implement automatic token refresh: ```python theme={null} import httpx import time class ServiceAccountClient: def __init__(self, client_id: str, client_secret: str, keycloak_url: str): self.client_id = client_id self.client_secret = client_secret self.keycloak_url = keycloak_url self._token = None self._expires_at = 0 def _refresh_token(self): response = httpx.post( f"{self.keycloak_url}/realms/master/protocol/openid-connect/token", data={ "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, } ) data = response.json() self._token = data["access_token"] self._expires_at = time.time() + data["expires_in"] - 60 # 60s buffer @property def token(self) -> str: if not self._token or time.time() >= self._expires_at: self._refresh_token() return self._token def get_headers(self, org_id: str, project_id: str, on_behalf_of: str | None = None) -> dict: headers = { "Authorization": f"Bearer {self.token}", "X-Org-Id": org_id, "X-Project-ID": project_id, } if on_behalf_of: headers["X-On-Behalf-Of"] = on_behalf_of return headers ``` ## Related Overview of all authentication methods including user tokens and OIDC. How permissions are checked for service account requests. Project-level isolation and the X-Project-ID header. # Webhooks (Planned) Source: https://docs.emergence.ai/platform/webhooks Event-driven webhook delivery with HMAC signing, retry logic, and SSRF protection for CRAFT. # Webhooks Webhooks enable event-driven communication between CRAFT and external systems. When a platform event occurs (such as a schedule trigger, agent completion, or data connection status change), the webhook system delivers a signed HTTP POST request to a configured endpoint. **Coming Soon**, The webhook system described on this page is planned but not yet implemented. This page documents the target architecture and capabilities for an upcoming release. Do not build integrations against these APIs until this warning is removed. ## Architecture The webhook system follows a publish-subscribe pattern built on **Redis Streams**, consistent with the platform's infrastructure-agnostic design: ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart TD E["Platform Event"] RS["Redis Streams
(event bus)"] WD["Webhook Dispatcher
HMAC sign → Deliver → Record → Retry"] EX["External System"] E --> RS --> WD --> EX classDef event fill:#56B4E9,stroke:#555555,color:#000 classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef proc fill:#E69F00,stroke:#555555,color:#000 classDef ext fill:#CC79A7,stroke:#555555,color:#000 class E event class RS infra class WD proc class EX ext ``` Redis Streams is used instead of cloud-specific message brokers (SQS, Pub/Sub) to avoid cloud-specific dependencies. Redis is already part of the infrastructure stack, so no additional dependencies are required. ## Security ### HMAC Payload Signing Every webhook delivery includes an HMAC-SHA256 signature in the request headers, allowing receivers to verify that the payload was sent by the platform and has not been tampered with: ```http theme={null} POST /webhook-endpoint HTTP/1.1 Content-Type: application/json X-Webhook-Signature: sha256=a1b2c3d4e5f6... X-Webhook-Timestamp: 1743696000 X-Webhook-Event: schedule.triggered {...payload...} ``` Receivers should: 1. Extract the timestamp and signature from headers 2. Reconstruct the signed payload: `{timestamp}.{body}` 3. Compute HMAC-SHA256 using the shared secret 4. Compare the computed signature with the received one (constant-time comparison) 5. Reject payloads older than a configurable tolerance window (e.g., 5 minutes) to prevent replay attacks ### SSRF Protection The webhook dispatcher includes protection against Server-Side Request Forgery (SSRF): * Private/internal IP ranges are blocked by default (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x) * DNS resolution is validated before delivery * Redirect following is disabled or limited to prevent redirect-based SSRF ## Retry Logic Failed webhook deliveries are retried with exponential backoff: | Attempt | Delay | Cumulative | | --------- | ---------- | ------------ | | 1st retry | 1 minute | 1 minute | | 2nd retry | 5 minutes | 6 minutes | | 3rd retry | 30 minutes | 36 minutes | | 4th retry | 2 hours | \~2.5 hours | | 5th retry | 12 hours | \~14.5 hours | A delivery is considered successful when the receiver responds with a 2xx status code. After all retries are exhausted, the delivery is marked as failed and recorded in the delivery log. ## Event Types The platform plans to support webhook notifications for the following event categories: | Category | Events | Description | | -------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------- | | **Schedules** | `schedule.triggered`, `schedule.failed` | Schedule execution events | | **Agents** | `agent.registered`, `agent.health_changed` | Agent lifecycle events | | **Assets** | `asset.created`, `asset.updated`, `asset.deleted` | Asset lifecycle events (files, artifacts, data connections) | | **Governance** | `organization.created`, `organization.deleted`, `project.created` | Tenant-boundary events | | **Data Connections** | `connection.status_changed`, `connection.verified` | Connection state changes | | **Deployments** | `deployment.started`, `deployment.completed`, `deployment.failed` | Deployment lifecycle | ## Webhook Payload Structure Webhook payloads follow a consistent envelope format: ```json theme={null} { "id": "evt-a1b2c3d4", "type": "schedule.triggered", "timestamp": "2026-04-03T06:00:00Z", "organization_id": "acme-corp", "project_id": "analytics-prod", "data": { "schedule_id": "sch-x1y2z3", "schedule_name": "Daily Analytics Refresh", "run_id": "run-m1n2o3" } } ``` | Field | Description | | ----------------- | ----------------------------------- | | `id` | Unique event ID for idempotency | | `type` | Event type identifier | | `timestamp` | ISO 8601 UTC timestamp of the event | | `organization_id` | Organization that owns the resource | | `project_id` | Project scope (if applicable) | | `data` | Event-specific payload | Use the event `id` field for idempotency. If you receive the same event ID multiple times (due to retries), process it only once. ## Multi-Tenancy Webhooks are scoped to organizations and optionally to projects, following the same isolation model as all platform resources: * Webhook configurations are owned by an organization and optionally scoped to a project * Events are only delivered for resources within the webhook's scope * Webhook secrets (for HMAC signing) are stored via the Governance Secrets API ## Next Steps Configure time-based triggers that generate webhook events. Monitor agent lifecycle events via webhooks. Receive notifications when connection status changes. Understand how webhook endpoints authenticate with the platform. # Release Notes Source: https://docs.emergence.ai/releases/index CRAFT platform changelog. Calendar-versioned monthly releases with curated, user-facing notes. # CRAFT Release Notes CRAFT ships on a monthly cadence using **calendar versioning** (`YYYY.MM`). Each release bundles updates across the platform; user-facing changes are summarized below in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) style. **Current Release: CRAFT 2026.06** CRAFT publishes a unified `YYYY.MM` release on or around the last business day of each calendar month. The release name reflects the month it ships in; if a release slips into the following month, the version follows the actual ship month. Underlying components continue to follow semantic versioning and may publish point releases between monthly bundles. *** ## 2026.06 *Released 2026-05-28* ### Added * **Snowflake key-pair and OAuth/SSO authentication for data connectors** — connect to Snowflake without long-lived passwords, aligning data ingestion with enterprise identity and key-rotation policies. See [Data Connections](/platform/data-connections). * **Org-level Snowflake data connection provisioning** — Snowflake connections can now be provisioned at the organization level, reducing per-project setup for teams sharing the same warehouse. See [Data Connections](/platform/data-connections). * **Memory asset filtering** — retrieve memories scoped to a specific dataset, schema, or table using the new `asset_fqn` field on the memories API, so agents can recall context without post-filtering a full memory set. See [Memory](/platform/memory-service). * **412 / 413 / 428 added to the API error reference** — the HTTP status table in [API Errors](/api-reference/errors) now documents Precondition Failed, Payload Too Large, and Precondition Required, matching what every service has been returning all along on agent-registration ETag/If-Match cycles and artifact uploads. ### Changed * **Default LLM is now `gemini-3.5-flash` (Vertex AI)** — across all framework tutorials, gateway configs, and per-call override examples. `gemini-3.5-flash` was GA'd at Google I/O 2026-05-19 and outperforms `gemini-3.1-pro` on 6 of 8 major benchmarks (including agentic MCP Atlas +5.4% and coding Terminal-Bench +6%) at 3.6× the speed. Secondary option shown for model agnosticity is now `claude-opus-4-8` on Vertex AI; OpenAI examples use `gpt-5.5`. See [Access LLMs](/guides/solution-dev/access-llms) and the [LLM Gateway](/platform/llm-gateway) operator guide. * **`gemini-3.1-pro-preview-customtools` re-scoped to specialized use only** — explicit warnings on every appearance: it is a fine-tuned variant of base `gemini-3.1-pro` that prioritizes registered custom functions over bash fallbacks; per Google's own guidance, only choose it when extensive custom tool calling is core to the agent (the model degrades on non-tool workloads if more than half of requests don't involve tools). Preview SLA, global endpoint only, Provisioned Throughput unsupported. * **SDK examples in the API reference now use the real client model** — the generated Python and TypeScript SDKs follow the openapi-python-client / openapi-generator pattern (`AuthenticatedClient` + per-endpoint module functions), not a fluent `GovernanceClient(...).foo.bar()` API that never existed. TypeScript npm scope corrected to `@emergence-ai/em-runtime-*-sdk`. See [API Authentication](/api-reference/authentication). * **Solution Developer Guide ports point at the gateway** — every direct host-port reference (`:8001`, `:8002`, `:8003`) in executable code and curl examples now uses the gateway path (`https:///api/{governance,assets,utils}/...`). The platform services all bind container port 8000; the gateway HTTPRoute fronts them. See [Local Development](/guides/solution-dev/local-development) and [Your First Agent](/guides/agent-author/your-first-agent). * **Agent-registration idempotency contract is now documented end-to-end** — `POST /assets/agents` returns 201 on first registration and 409 `RESOURCE_ALREADY_EXISTS` on a re-POST with the same name in the same project; `PUT` requires an `If-Match` header (428 Precondition Required without; 412 Precondition Failed with a stale ETag; 200 with the current one). See [Your First Agent](/guides/agent-author/your-first-agent). * **RBAC configuration rewritten against the real permissions API** — Steps 2–4 of the RBAC guide now use the live `/governance/permissions/{grant,revoke,check,set-parent,delete-all,accessible-objects}` endpoints. `check` is a `GET` with query parameters; grants and revokes are `POST` with `{resource_type, resource_id, subject, relation}` bodies. See [RBAC Configuration](/guides/rbac-configuration). * **Data connection registration documents the real schema** — the field is `connection_type` (not `type`), the enum value is `postgres` (not `postgresql`), and configuration goes under `database_config` / `storage_config` / `filesystem_config` depending on connector kind, with credentials as a separate top-level block. Pre-save validation lives at `POST /assets/data/verify`; post-save validation at `POST /assets/data/{resource_uri}/verify`. See [Data Source Setup](/guides/data-source-setup). * **Environment-variable conventions clarified for Vertex AI** — `VERTEXAI_PROJECT` / `VERTEXAI_LOCATION` (the LiteLLM convention used by CRAFT's reference Helm chart) is the canonical form; LiteLLM also accepts the Google Cloud SDK standards `GOOGLE_CLOUD_PROJECT` / `GOOGLE_CLOUD_LOCATION` set automatically by `gcloud auth application-default login`. See the [LLM Gateway](/platform/llm-gateway) operator guide. * **Top-bar nav now has icons** matching the left-nav style across all 8 tabs. * **Refreshed platform UI components** — the runtime interface now includes updated Avatar, Accordion, ContextualMenu, SegmentedControl, and Tooltip components, improving visual consistency across the platform. * **Semiconductor v0.3.9 cycle** — Knowledge tab (Documents + Agent Insights), equipment factor analysis embedded directly in generated reports, fab-dashboard-contextual chat answers, Excursion Reconciliation Agent (equipment signals vs yield outcomes), pre-generated report exports, chat runtime progress bar, wafer-map heatmaps, mid-stream agent-task cancellation, and \~30 incremental fixes. See [Semiconductor](/semiconductor/overview). ### Fixed * **Memory API documentation corrected to actual `/utils/` endpoints** — The Memory Service pages previously documented non-existent `/v2/api/memories` paths. Memory is implemented within the Utils service (`em-runtime-utils`); the correct endpoints are `POST /api/utils/context-packs/{name}/memories` (create), `GET /api/utils/memories` (list), `GET /api/utils/memories/{id}` (retrieve), `PATCH /api/utils/memories/{id}` (update), and `DELETE /api/utils/memories/{id}` (soft-delete). Context pack creation requires `pack_type` (not `type`) and pagination uses `page_size` (not `limit`). All requests require an `X-Project-ID` header for project scoping. If you built against the previously documented paths, update to the `/utils/` routes. See [Memory Service](/platform/memory-service) and [Memory Integration](/guides/memory-integration). * **Catalog summary updates** — catalog entity summaries now update correctly across all API call paths. * **Natural-language SQL generation stability** — resolved a crash when table selections were supplied without schemas, and tightened validation on chart type fields so malformed model responses no longer break query generation. See [Data Insights](/data-insights/overview). * **Gemini embedding compatibility** — embeddings produced by Gemini models are now sized to fit the vector store, preventing ingestion failures on memory and catalog writes. * **Data table interactions** — corrected spacing and search behavior in runtime data tables so filtering and selection behave as expected. * **`em-service` chart version pin corrected repo-wide** — `helm install ... --version 0.0.16` was returning "manifest unknown" because only `0.0.15` is published to the OCI registry. The earlier fix covered Quickstart and the em-service chart reference; the pin is now corrected across the whole Solution Developer Guide (starter templates, register-a-solution, package-and-deploy, overview) and the Tier 1 CI fixtures, so every scaffold pins the actually-published `0.0.15`. See [Quickstart](/guides/solution-dev/quickstart) and [em-service chart](/deployment/helm/em-service). * **`hello-solution` resource naming in Quickstart** — adding `api.fullnameOverride: hello-solution-api` to the `values.yaml` ensures `kubectl port-forward svc/hello-solution-api` resolves (without the override, em-service defaults the resource name to the release name and the documented commands return `NotFound`). *** ## 2026.05 *Released 2026-05-13* ### Added * **Catalog foreign-key graph and DDL storage** — the data catalog now traces relationships between entities through foreign-key edges (including soft FKs) and supports path-finding queries, and each catalog entity retains its `CREATE TABLE` DDL alongside profile and lineage metadata. Together these let agents and SQL-generation pipelines navigate schemas without re-deriving structure on every call. See [Data Connections](/platform/data-connections). * **MCP tools for schema exploration** — `get_schema`, `search_schema`, and `sample_data` let any MCP-aware client introspect catalog structure and preview rows from a connected warehouse, so chat surfaces outside the platform can ground answers in live schema rather than stale copies. See [MCP Server](/platform/mcp-server). * **MCP tools for natural-language analytics** — `generate_sql`, `execute_query`, `resolve_term`, and `generate_plotly_chart` expose Data Insights through MCP, so external assistants can ask questions in natural language and receive SQL, results, and charts without leaving their conversation. See [Chat with Data](/data-insights/chat-with-data). * **Wildcard catalog field search** — leading and both-ends wildcards (`*term*`, `term*`) now match field names, so columns can be located from a fragment rather than the full identifier. See [Data Connections](/platform/data-connections). * **HTTP observability metrics with swappable OTEL backends** — every platform service emits consistent HTTP latency, error, and throughput metrics routable to any OpenTelemetry-compatible backend, and a new usage-event endpoint records per-tenant API consumption for reporting. See [OpenTelemetry](/deployment/observability/opentelemetry). * **Project selector in the sidebar** — the runtime UI footer now exposes the active project and a switcher, removing a navigation hop when working across multiple projects. * **Semiconductor Knowledge tab** — a Documents and Agent Insights surface in the fab analytics dashboard so engineers can browse SOPs, ingest curated reference material, and review agent-generated analysis alongside live runs. See [Semiconductor](/semiconductor/overview). * **Equipment factor analysis with report integration** — fab analytics agents now render equipment factor visualizations directly inside generated reports, surfacing tool-level contributions to yield without a separate analysis step. See [Semiconductor](/semiconductor/overview). * **Chat with context from the fab dashboard** — the semiconductor chat agent answers questions grounded in the current dashboard view, so analysts can ask follow-ups without restating which run, wafer, or excursion they are inspecting. See [Semiconductor](/semiconductor/overview). * **Excursion Reconciliation Agent** — compares equipment signals against yield outcomes (including yield-only paths when equipment data is partial) to triangulate root causes for production excursions. See [Semiconductor](/semiconductor/overview). * **Pre-generated semiconductor report exports** — workflow runs now pre-generate downloadable report artifacts at completion instead of waiting for an export request, making large run results immediately shareable. * **Chat runtime progress bar** — long-running semiconductor chat turns now show turn-based progress, so users get visible feedback during multi-minute analysis runs. ### Changed * **Service account secrets use self-descriptive key names** — autonomous workloads read `client_id` and `client_secret` from explicitly-named secret fields rather than generic ones, making rotation and audit clearer. See [Authentication](/platform/authentication). * **Autonomous service accounts resolve consistently in audit and authorization** — request context for non-interactive callers now returns the service account's `client_id` as `user_id`, so authorization checks and audit logs trace back to the right principal regardless of whether the caller is a person or a service. See [Authorization](/platform/authorization). * **Metadata search responses include the publishing service** — each search result now carries a `service_name` field, so multi-service catalogs are easier to navigate and disambiguate. * **Data Readiness gains scoped read access to project metadata** — a new `service_metadata_reader` relation in the project authorization schema grants the Data Readiness service the minimum permissions it needs without broadening other service roles. See [Authorization](/platform/authorization). * **HTTP metrics capture the routed path, not the raw URI** — observability middleware now records the resolved route template, so dashboards group requests by endpoint instead of fragmenting on path parameters. See [OpenTelemetry](/deployment/observability/opentelemetry). * **Faster semiconductor API responses** — caching and bulk-query consolidation cut latency on analytics endpoints under sustained dashboard load, so frequently-viewed runs and excursions render faster on subsequent visits. * **Iceberg ingest path for semiconductor data generation** — the v2 data-generation workflow can now write directly into Apache Iceberg, simplifying integration with lakehouse storage for downstream analytics. See [Semiconductor](/semiconductor/overview). ### Fixed * **OIDC discovery metadata at the issuer-derived path** — well-known OIDC metadata is now served at the path RFC 8414 clients probe (including Claude Code), so MCP authentication flows complete without manual endpoint configuration. See [MCP Server](/platform/mcp-server). * **Whitespace-only catalog search returns an empty result set** — a blank or whitespace query no longer raises an error; it returns no rows, matching the behavior of every other search input. * **Navigation no longer crashes after workflow completion** — moving away from a completed workflow run in the runtime UI no longer throws. * **"Clear All" resets every score type** — clearing scores in the runtime now resets metadata scores through the unified scores endpoint, alongside the other score categories. * **Chat artifact table styling** — table outlines and header backgrounds render correctly in chat artifact previews. * **Semiconductor chat answer accuracy** — fixed a case where numbered option selections were misinterpreted as entity IDs, and added structured error parsing so agent-task failures surface readable messages instead of opaque tracebacks. See [Semiconductor](/semiconductor/overview). * **Reliable wafer-map visualizations in chat** — wafer maps now render as heatmaps rather than scatter plots, restoring readable spatial distributions in chat answers. * **Mid-stream cancellation for semiconductor agent tasks** — disconnecting the client mid-stream now correctly cancels the in-flight agent task, freeing compute and avoiding orphaned work. ### Security * **`@tanstack` packages pinned against a supply chain attack** — exact version pins prevent installation of compromised `@tanstack/*` versions discovered in the npm registry, hardening the runtime UI build against dependency confusion. *** ## 2026.04 *Released 2026-04-30* ### Added * **Conversational data analysis (chat-with-data)** — ask natural-language questions across your registered data assets and receive streaming answers with auto-generated SQL, charts, tables, and downloadable Python code. Sessions support multi-turn follow-ups, artifact attachments, and resumable event replay if the connection drops. See [Chat with Data](/data-insights/chat-with-data). * **Snowflake data connector** — connect to Snowflake warehouses for cataloging, profiling, and natural-language querying alongside existing PostgreSQL and Redshift sources. See [Data Connections](/platform/data-connections). * **Automated data intelligence enrichment** — a new eight-step pipeline runs after profiling to produce table and column descriptions, semantic classifications (Fact/Dimension/Reference), join proposals, sample questions, glossary terms, and text-pattern detections. Each finding is persisted as a searchable memory so downstream agents can reason over your catalog without re-discovery. See [Data Enrichment](/data-governance/data-enrichment). * **Business glossary backed by the memory service** — vocabulary and concept terms now live in context packs with verification status, tagging, and back-references to the tables where each term appears. See [Memory](/platform/memory-service). * **Org-level data connection provisioning** — connections can be registered once at the organization level and reused across projects, reducing per-project setup for shared warehouses. See [Data Connections](/platform/data-connections). * **Audit logging via OpenTelemetry** — platform service and workflow audit events stream through the OTEL collector alongside traces and metrics, unifying observability for compliance-sensitive deployments. See [OpenTelemetry](/deployment/observability/opentelemetry). * **Self-Improvement evals harness for the semiconductor stack** — internal evals infrastructure for the fab analytics agents, exercising agent behaviour against curated scenarios so regressions are caught before they ship. See [Semiconductor](/semiconductor/overview). * **Client-dashboard observability for semiconductor analytics** — the runtime UI's semiconductor client dashboards now emit structured telemetry to the platform OpenTelemetry pipeline, so dashboard performance and agent invocations are visible alongside backend services. * **Excursion editing and tabbed run details (semiconductor)** — engineers can now edit excursion records in place and navigate run details through a tabbed interface, reducing click depth during incident review. See [Semiconductor](/semiconductor/overview). ### Changed * **Breaking: Data quality workflow triggers no longer accept `resource_type`, `resource_uri`, or `database_type`** — tag generation, rule generation, and execution now derive these from the asset FQN, organization, and project. Tag generation accepts a new optional `industry_info` parameter for domain-specific tagging. Callers must remove the deprecated fields from trigger payloads. See [Data Governance Workflows](/data-governance/workflows). * **Data quality results now apply on completion** — DQ execution, DQ assessment, and metadata assessment workflows no longer create a pending approval; results are written directly when the workflow finishes, eliminating the manual approval step. See [Data Governance Workflows](/data-governance/workflows). * **Profiling, Metadata Assessment, and DQ Assessment workflows are now table- or schema-scoped only** — database-wide enrichment moves to the new Data Intelligence workflow, which orchestrates LLM passes across the entire catalog in a single run. See [Data Governance Workflows](/data-governance/workflows). * **Workflow run listings support multi-status filtering and multi-column sorting** — query workflow history with expressions like `?status=running,failed&sort=status:desc,started_at:asc`, and group workflows by category (ingestion, enrichment, data\_quality, assessment) in the configuration list. See [Data Governance Workflows](/data-governance/workflows). * **Column profiles include distribution analysis** — value frequency counts for low-cardinality columns, percentiles for numerics, length and case patterns for text, temporal ranges, and JSON structure breakdowns, controlled by the `DISTRIBUTION_ANALYSIS_ENABLED` flag. See [Data Profiling](/data-governance/data-profiling). * **Refreshed conversational interface for Talk2Data** — streaming UX improvements, an artifact picker panel with infinite scroll, active context bar, session context management, and reliable auto-reconnection on dropped streams. See [Chat with Data](/data-insights/chat-with-data). * **Runtime UI cycle: dashboard navigation polish, agent-card flows, and dozens of incremental fixes** — the runtime UI shipped fifty point releases this cycle covering dashboard navigation flow, agent registration UI, project-context propagation in SDK requests, table interaction polish, and component refresh (Avatar, Accordion, ContextualMenu, Tooltip). * **Resolved agent-card URLs in the semiconductor A2A client** — the fab analytics agents now use the agent's resolved well-known URL rather than its display-name URL, fixing edge cases when registering or invoking agents from custom endpoints. See [Semiconductor](/semiconductor/overview). * **More accurate Otsu threshold for semiconductor yield analysis** — added a MAD-based geometric-mean correction that prevents threshold inflation on noisy distributions, producing more reliable defect/yield segmentation. ### Fixed * **PostgreSQL ingestion to SSL-enforcing databases** — connections to AWS RDS and other managed PostgreSQL deployments no longer fail with `no pg_hba.conf entry … no encryption`; the connection pool now negotiates SSL with the same semantics as `sslmode=require`. See [Data Connections](/platform/data-connections). * **Data quality scorecards reset correctly after a clear** — DQ Score now returns N/A immediately after clearing values and reflects fresh results on the next rule execution, instead of staying pinned at 100%. * **Schema search filters by table name** — searching within a schema returns only tables whose names contain the query, instead of returning every table in the schema. * **Issues requiring attention excludes deleted datasets** — the home dashboard no longer surfaces violations for soft-deleted tables in either the list or the count. * **Profiling stability on Redshift** — large-value aggregations on event-count columns no longer overflow, and distribution analysis no longer crashes on boolean columns. See [Data Profiling](/data-governance/data-profiling). * **Catalog summary updates** — entity summaries now refresh across all API call paths, eliminating stale text after asset updates. ### Security * **MIME type validation on artifact and file uploads** — uploaded files are validated against an allowlist before storage, preventing disguised content from reaching the catalog or downstream agents. * **SQL injection hardening in dataset search** — catalog search escapes user-supplied wildcards and validates query input before composing `ILIKE` filters. * **Stricter data connection name validation** — connection names containing special characters that could break downstream SQL or filesystem paths are rejected at the API boundary. *** ## 2026.03 *Released 2026-03-31* ### Added * **Intelligent context for AI agents** — an 8-step LLM enrichment pipeline runs automatically after profiling, producing asset-granular memories (semantic layer, table summaries, sample questions, JOIN proposals, text patterns) that agents can retrieve at query time. See [Data Enrichment](/data-governance/data-enrichment). * **Memory** — agents can now store and retrieve structured context packs and typed memories (glossary, exemplar, summary, observation, ontology, policy, join) through the Utils service, so they recall task-specific knowledge across sessions. See [Memory](/platform/memory-service). * **Business Glossary** — glossary terms are now persisted as first-class memories with full CRUD, verification, and tagging, replacing the previous placeholder. See [Data Governance](/data-governance/overview). * **Chat with Data** — a new conversational interface for asking natural-language questions of your data, with multi-turn sessions, server-sent responses, a durable per-turn event log for replay, and auto-generated session titles. See [Chat with Data](/data-insights/chat-with-data). * **Column distribution analysis** — column profiles now include histograms: value-frequency counts for low-cardinality columns, percentiles for high-cardinality numerics, length and case analysis for text, temporal ranges, and JSON structure analysis. See [Data Profiling](/data-governance/data-profiling). * **Data Quality approval workflows** — generated DQ tags and rules can be previewed (including SQL and validation status) before being persisted, so reviewers approve before commit. See [Data Governance](/data-governance/overview). * **Hardened semiconductor code-run execution** — the fab analytics code-run service now handles out-of-memory failures gracefully without crashing the agent, with improved observability into long-running runs and corrected log message streaming so users see live progress instead of buffered batches. See [Semiconductor](/semiconductor/overview). * **Persistent client-dashboard auth across tabs (semiconductor)** — fab analytics users no longer need to re-authenticate when opening run details in a new browser tab. * **Runtime UI: initial dashboard navigation, chart controls, and agent integration** — the runtime UI shipped 19 point releases this cycle covering the foundational dashboard navigation flow, chart-control polish, log message streaming, and authentication persistence across the sidebar. ### Changed * **Event-driven ingestion and auto-chained profiling** — creating a data connection now automatically triggers ingestion via the platform event bus, and ingestion auto-chains into profiling on completion. No more manual workflow kickoff. See [Data Connections](/platform/data-connections). * **Simpler DQ workflow inputs** — tag generation, rule generation, and execution no longer require `resource_type`, `resource_uri`, or `database_type` parameters — these are derived internally from the asset FQN. Tag generation also accepts an optional `industry_info` parameter for domain-specific tagging. See [Data Governance](/data-governance/overview). * **DQ execution, DQ assessment, and metadata assessment no longer require approval** — results are available immediately on completion, since these are read-only analyses. Tag and rule *generation* still require approval (preview before commit). See [Data Governance](/data-governance/overview). * **Faster metadata assessments at scale** — scoring and violation detection now run in-memory against the catalog SDK rather than issuing \~28 database queries per asset, sized to handle large catalogs without timing out. See [Data Governance](/data-governance/overview). * **OpenTelemetry instrumentation across the platform** — runtime services now emit traces, metrics, and structured logs to your OpenTelemetry collector, supporting end-to-end request tracing. See [OpenTelemetry](/deployment/observability/opentelemetry). * **Richer catalog responses and filters** — catalog entities now include `row_count`, `column_count`, and `table_count`; metadata responses support `include_children` to expand nested assets; asset list endpoints accept `key_prefix` and `owner_id` filters; tag operations support bulk get/store/delete. See [Data Connections](/platform/data-connections). ### Fixed * **Cross-tenant resource URI collisions in authorization** — the authorization engine now keys resource URIs by tenant, so the same `resource_uri` in different tenants no longer shares permissions. See [Authorization](/platform/authorization). * **Catalog re-ingestion no longer blocked after database-level deletes** — removing assets directly from the database used to leave orphan references that prevented re-ingestion through the API; re-ingestion now succeeds cleanly. See [Data Connections](/platform/data-connections). * **Data connection secrets are cleaned from the secrets backend on deletion** — deleting a connection now removes its credentials from secret storage, eliminating orphaned secrets. See [Data Connections](/platform/data-connections). * **Multi-replica migration race condition** — concurrent platform replicas no longer race when applying database migrations on startup; an advisory lock now serializes migration runs safely. * **More predictable workflow approval and execution** — workflow approval no longer fails on Keycloak token refresh, status updates no longer deadlock under concurrent load, and workflow runs against external Keycloak no longer fail with JWT issuer mismatches. ### Removed * **MinIO removed as the Chat with Data artifact backend** — query results (Parquet, charts, JSON) now live in the platform Artifacts API, authenticated via service-worker tokens. Operators no longer need to provision MinIO alongside Chat with Data. See [Chat with Data](/data-insights/chat-with-data). * **Debug `/config` endpoint removed from production** — internal configuration is no longer exposed over HTTP. ### Security * **Streaming file uploads** — large file and artifact uploads now stream to storage rather than buffering the entire payload in memory, removing a denial-of-service vector on multi-gigabyte uploads. See [Data Connections](/platform/data-connections). * **Stricter resource URI validation in DQ workflows** — DQ tag, rule, and execution workflows now validate resource URIs and propagate organization and project context end-to-end, preventing cross-project asset references. *** ## 2026.02 *Released 2026-02-27* ### Added * **Data Quality workflows for Data Governance** — automated DQ tag generation, rule generation, and rule execution, plus scorecard, trend, and detail endpoints for tracking quality metrics across assets. See [Data Governance](/data-governance/overview) and [Data Profiling](/data-governance/data-profiling). * **Talk to Data foundation** — initial release of the multi-agent system that powers natural-language conversations with structured data sources, including shared libraries and orchestration scaffolding for downstream solutions. See [Data Insights](/data-insights/overview) and [Chat with Data](/data-insights/chat-with-data). * **Table and column profiling with Amazon Redshift support** — a new Profiler workflow scans tables and columns to seed governance assessments, and a Redshift adapter extends profiling beyond Snowflake and Postgres. See [Data Connections](/platform/data-connections). * **Workflow approval flow with real-time status streaming** — Data Governance workflows now support a reviewer approval step before runs are finalized (with reset-to-pending), and a Server-Sent Events stream on workflow endpoints replaces client-side polling for status updates. See [Workflows](/data-governance/workflows). * **Bulk catalog metadata endpoints and entity versioning** — update metadata for many catalog entities in a single request, and track changes over time with major/minor versioning on catalog metadata entities. * **Multivariate excursion detection and report visualizations for the semiconductor solution** — detect lot excursions across correlated variables (not just single-metric thresholds), and generate charts directly inside fab analytics reports through a new visualization agent. See [Semiconductor](/semiconductor/overview). ### Changed * **Connection health visibility** — data connections now expose `status` and `status_message`, and list responses include `owner_id`, so administrators can diagnose connection failures and identify owners without extra requests. See [Data Connections](/platform/data-connections). * **More descriptive workflow status messages** — Prefect-backed workflows now emit structured status messages from tasks and lifecycle hooks, so operators see meaningful progress instead of opaque task names. * **Standardized pagination and error envelope across Data Governance endpoints** — consistent page, size, and cursor semantics and a unified error response shape throughout the API surface. See [Data Governance](/data-governance/overview). * **Breaking: Data Quality workflow APIs split rule generation from execution** — DQ rule generation and execution are now distinct workflow steps; integrations that previously called a single combined endpoint must adopt the two-step generate-then-execute pattern. See [Workflows](/data-governance/workflows). ### Fixed * **Multi-realm Keycloak bootstrap and CORS coverage** — bootstrap against newer Keycloak versions now succeeds, and CORS preflight is enabled on the remaining platform APIs that previously rejected browser requests. See [Authentication](/platform/authentication). * **Database migration safety and connection-pool stability** — migrations now fail fast when multiple Alembic heads are detected, preventing partial schema upgrades, and nested savepoints in workflow event creation are now committed on success, eliminating a slow connection-pool leak under sustained load. * **SSE streaming robustness** — malformed `Last-Event-ID` header values now return 400 instead of crashing the streaming endpoint with a 500. ### Removed * **Breaking: Legacy `domain_id` and `client_id` fields removed from data catalog entities** — these tenant-extension fields are no longer recognized in catalog payloads; callers must migrate to the standard organization and project model. See [Data Connections](/platform/data-connections). *** ## 2026.01 *Released 2026-01-31* ### Added * **Data connection management API** — programmatically register, list, update, and remove PostgreSQL and Redshift connections, with built-in connectivity verification so misconfigured credentials are caught before a workflow runs. See [Data Connections](/platform/data-connections). * **Object storage APIs** — first-class clients and endpoints for managing object-storage assets across deployments, so agents can read and write blobs without bespoke integration code. * **Scoped secrets at organization, project, and resource levels** — store and retrieve secrets at the granularity your workflow actually needs, with permission checks that inherit from the owning resource so secret access stays consistent with the rest of your authorization model. See [Authorization](/platform/authorization). * **Agent guardrails** — opt-in safety checks on agent inputs and outputs, providing a foundation for content moderation, PII handling, and policy enforcement on agentic workflows. See [Agents](/platform/agents). * **Data generation agent for semiconductor workflows** — an interactive agent with web-search capability guides fab engineers through synthetic dataset generation, including reproducible runs via configurable seeds. See [Semiconductor](/semiconductor/overview). * **Data catalog and metadata enrichment workflows** — LLM-powered metadata enrichment over a versioned catalog of databases, schemas, tables, and columns; ingestion runs are tracked end-to-end with configurable retry behavior and live status reporting. See [Data Enrichment](/data-governance/data-enrichment). ### Changed * **A2A protocol for semiconductor chat** — chat services now run on the standard A2A SDK, with end-user authentication propagated through each agent request, aligning the semiconductor solution with the platform's agent-to-agent communication model. See [Semiconductor](/semiconductor/overview). * **Unified bootstrap across platform services** — a single bootstrap flow now provisions identity realms, organizations, permission grants, and database state in one pass, and surfaces project-level readiness in the bootstrap status response. * **Stateless yield-excursion workflow** — yield-excursion pipelines now pass data via explicit dictionaries between steps rather than shared state, making runs easier to reason about, retry, and observe. See [Semiconductor](/semiconductor/overview). * **Standardized workflow naming and a service-ingestion workflow type** — workflow names follow a single convention across ingestion, enrichment, and analysis runs, and service ingestion is now a first-class workflow type. See [Data Governance Workflows](/data-governance/workflows). ### Fixed * **Pearson correlation in excursion analysis** — corrected a case where the Pearson coefficient was applied to inputs it shouldn't have been, restoring expected correlation results in yield-excursion runs. See [Semiconductor](/semiconductor/overview). * **Windows compatibility for self-improvement runs** — resolved path and environment issues that prevented self-improvement workflows from running on Windows hosts. * **Local Docker Compose startup for semiconductor services** — fixed build and runtime errors that blocked bringing up all semiconductor services together, easing local end-to-end testing. * **Reports now reference the analysis run, not the underlying orchestrator run** — generated reports link to the correct analysis run ID, so report URLs match what users see in the analysis history. * **Catalog asset generation** — addressed a case where catalog asset generation could silently no-op under certain bootstrap states. ### Security * **Permission enforcement on all secrets endpoints** — every create, read, update, and delete on the secrets API is now gated on the caller's authorization against the owning resource, closing a gap where checks based on secret type alone could be bypassed. See [Authorization](/platform/authorization). * **Container base images pinned to immutable digests** — workflow runner images now reference specific tool versions instead of `:latest`, preventing supply-chain drift if a published tag is moved or replaced. * **Hardened API surface** — standardized exception handling and additional input validation across runtime services reduce the risk of information disclosure via unhandled errors. See [API Reference](/api-reference/introduction). # SDKs & Client Libraries Source: https://docs.emergence.ai/sdks/overview Auto-generated Python and TypeScript SDKs for CRAFT APIs # SDKs & Client Libraries CRAFT provides auto-generated SDKs for **Python** and **TypeScript**, built from OpenAPI specifications on every release. Type-safe Python client with async support, Pydantic models, and full API coverage. Fully typed TypeScript client for Node.js and browser environments. ## Available SDKs | Package | Python (PyPI) | TypeScript (npm) | API | | -------------- | --------------------------- | ----------------------------------------- | ------------------------------------------------- | | **Governance** | `em-runtime-governance-sdk` | `@emergence-ai/em-runtime-governance-sdk` | Organizations, Projects, Permissions | | **Assets** | `em-runtime-assets-sdk` | `@emergence-ai/em-runtime-assets-sdk` | Data Connections, Artifacts, Files, Models | | **Utils** | `em-runtime-utils-sdk` | `@emergence-ai/em-runtime-utils-sdk` | Data Catalog, Scheduling, Context Packs, Memories | ## SDK Generation Pipeline SDKs are auto-generated from OpenAPI specs using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) and the OpenAPI Generator for TypeScript. The pipeline runs on every final release: FastAPI apps export their schemas via `app.openapi()` at build time. OpenAPI specs are fed to code generators producing typed clients with models, API classes, and authentication helpers. Python SDKs publish to Google Artifact Registry. TypeScript SDKs publish to the `@emergence-ai` npm scope. SDK versions match the platform release version (e.g., platform v5.28.0 → SDK v5.28.0). SDK versions may lag behind the latest platform service versions. For example, the Runtime UI may use Assets SDK v5.27.0 while the Assets service is at v5.28.0. SDKs are backward-compatible, newer service versions serve older SDK clients without issue. Check the release notes for current versions. ## Authentication All SDKs use JWT bearer token authentication via Keycloak: ```python theme={null} from em_runtime_governance_sdk import AuthenticatedClient client = AuthenticatedClient( base_url="https://your-instance.emergence.ai", token="your-jwt-token" ) ``` ```typescript theme={null} import { Configuration, OrganizationsApi } from '@emergence-ai/em-runtime-governance-sdk'; const config = new Configuration({ basePath: 'https://your-instance.emergence.ai', accessToken: 'your-jwt-token', }); const api = new OrganizationsApi(config); ``` ## Registry Configuration SDKs are hosted on Google Artifact Registry (private). Configure your package manager: ```bash theme={null} # Add the Artifact Registry index uv add em-runtime-governance-sdk --index-url https://us-central1-python.pkg.dev/aip-artifacts-store/aipy/simple/ # Or with pip pip install em-runtime-governance-sdk \ --index-url https://us-central1-python.pkg.dev/aip-artifacts-store/aipy/simple/ ``` ```bash theme={null} # Configure the @emergence-ai scope echo "@emergence-ai:registry=https://us-central1-npm.pkg.dev/aip-artifacts-store/aip-npm/" >> .npmrc # Install npm install @emergence-ai/em-runtime-governance-sdk ``` Registry authentication requires Google Cloud credentials. See your platform admin for access tokens or Workload Identity setup. ## Next Steps Installation, usage patterns, error handling Setup, type safety, async patterns Interactive API playground with all endpoints Keycloak OIDC setup and token management # Python SDK Source: https://docs.emergence.ai/sdks/python Type-safe Python client for CRAFT APIs # Python SDK The CRAFT Python SDK provides type-safe, auto-generated clients for all platform APIs. Built with Pydantic models and httpx for async support. ## Installation ```bash theme={null} # Install all SDKs uv add em-runtime-governance-sdk em-runtime-assets-sdk em-runtime-utils-sdk \ --index-url https://us-central1-python.pkg.dev/aip-artifacts-store/aipy/simple/ # Or install individually uv add em-runtime-governance-sdk \ --index-url https://us-central1-python.pkg.dev/aip-artifacts-store/aipy/simple/ ``` ## Quick Start ### Create a Client ```python theme={null} from em_runtime_governance_sdk import AuthenticatedClient # Create an authenticated client client = AuthenticatedClient( base_url="https://your-instance.emergence.ai/governance", token="your-jwt-token", ) ``` ### List Organizations ```python theme={null} from em_runtime_governance_sdk.api.organizations import list_organizations from em_runtime_governance_sdk.models import OrganizationListResponse response = list_organizations.sync(client=client) for org in response.data: print(f"{org.id}: {org.name}") ``` ### Create a Project ```python theme={null} from em_runtime_governance_sdk.api.projects import create_project from em_runtime_governance_sdk.models import ProjectCreate project = create_project.sync( client=client, body=ProjectCreate( name="My Project", description="Data analysis workspace", ), ) print(f"Created project: {project.id}") ``` ### Create a Data Connection ```python theme={null} from em_runtime_assets_sdk import AuthenticatedClient as AssetsClient from em_runtime_assets_sdk.api.data import create_data_connection from em_runtime_assets_sdk.models import DataConnectionCreate assets_client = AssetsClient( base_url="https://your-instance.emergence.ai/assets", token="your-jwt-token", ) connection = create_data_connection.sync( client=assets_client, body=DataConnectionCreate( name="analytics-db", connection_type="postgres", description="Analytics PostgreSQL database", ), ) ``` ## Async Usage All SDK methods support async via the `.asyncio()` variant: ```python theme={null} import asyncio from em_runtime_governance_sdk.api.organizations import list_organizations async def main(): response = await list_organizations.asyncio(client=client) for org in response.data: print(f"{org.id}: {org.name}") asyncio.run(main()) ``` ## Error Handling ```python theme={null} from em_runtime_governance_sdk.types import Response from httpx import HTTPStatusError try: response = list_organizations.sync_detailed(client=client) if response.status_code == 200: data = response.parsed elif response.status_code == 401: print("Token expired — refresh and retry") elif response.status_code == 403: print("Insufficient permissions") except HTTPStatusError as e: print(f"HTTP error: {e.response.status_code}") ``` ## Package Reference | Package | Covers | Import | | --------------------------- | ------------------------------------------------- | ------------------------------------------- | | `em-runtime-governance-sdk` | Organizations, Projects, Permissions, Roles | `from em_runtime_governance_sdk import ...` | | `em-runtime-assets-sdk` | Artifacts, Data Connections, Files, Models | `from em_runtime_assets_sdk import ...` | | `em-runtime-utils-sdk` | Data Catalog, Scheduling, Context Packs, Memories | `from em_runtime_utils_sdk import ...` | ## Next Steps TypeScript/JavaScript client guide Interactive API playground # TypeScript SDK Source: https://docs.emergence.ai/sdks/typescript Fully typed TypeScript client for CRAFT APIs # TypeScript SDK The CRAFT TypeScript SDK provides fully typed clients for all platform APIs, with support for Node.js and browser environments. ## Installation ```bash theme={null} # Configure the @emergence-ai npm scope echo "@emergence-ai:registry=https://us-central1-npm.pkg.dev/aip-artifacts-store/aip-npm/" >> .npmrc # Install all SDKs npm install @emergence-ai/em-runtime-governance-sdk \ @emergence-ai/em-runtime-assets-sdk \ @emergence-ai/em-runtime-utils-sdk ``` ## Quick Start ### Configure the Client ```typescript theme={null} import { Configuration, OrganizationsApi, ProjectsApi, } from '@emergence-ai/em-runtime-governance-sdk'; const config = new Configuration({ basePath: 'https://your-instance.emergence.ai/governance', accessToken: 'your-jwt-token', }); const orgsApi = new OrganizationsApi(config); const projectsApi = new ProjectsApi(config); ``` ### List Organizations ```typescript theme={null} const { data: orgs } = await orgsApi.listOrganizations(); orgs.data.forEach(org => { console.log(`${org.id}: ${org.name}`); }); ``` ### Create a Project ```typescript theme={null} const { data: project } = await projectsApi.createProject({ projectCreate: { name: 'My Project', description: 'Data analysis workspace', }, }); console.log(`Created project: ${project.id}`); ``` ### Create a Data Connection ```typescript theme={null} import { Configuration as AssetsConfig, DataApi, } from '@emergence-ai/em-runtime-assets-sdk'; const assetsConfig = new AssetsConfig({ basePath: 'https://your-instance.emergence.ai/assets', accessToken: 'your-jwt-token', }); const dataApi = new DataApi(assetsConfig); const { data: connection } = await dataApi.createDataConnection({ dataConnectionCreate: { name: 'analytics-db', connectionType: 'postgres', description: 'Analytics PostgreSQL database', }, }); ``` ## Error Handling ```typescript theme={null} import { AxiosError } from 'axios'; try { const { data } = await orgsApi.listOrganizations(); } catch (error) { if (error instanceof AxiosError) { switch (error.response?.status) { case 401: console.error('Token expired — refresh and retry'); break; case 403: console.error('Insufficient permissions'); break; default: console.error(`API error: ${error.response?.status}`); } } } ``` ## Token Refresh Pattern ```typescript theme={null} import { Configuration } from '@emergence-ai/em-runtime-governance-sdk'; // Dynamic token provider const config = new Configuration({ basePath: 'https://your-instance.emergence.ai/governance', accessToken: async () => { // Fetch fresh token from Keycloak const token = await refreshKeycloakToken(); return token; }, }); ``` ## Package Reference | Package | Covers | Scope | | ----------------------------------------- | ------------------------------------------------- | ------------ | | `@emergence-ai/em-runtime-governance-sdk` | Organizations, Projects, Permissions | `governance` | | `@emergence-ai/em-runtime-assets-sdk` | Artifacts, Data Connections, Files, Models | `assets` | | `@emergence-ai/em-runtime-utils-sdk` | Data Catalog, Scheduling, Context Packs, Memories | `utils` | ## Next Steps Python client guide Interactive API playground # Authentication Source: https://docs.emergence.ai/security/authentication Deep dive into Keycloak authentication, OIDC flows, JWT token management, and machine-to-machine authentication on CRAFT. # Authentication CRAFT uses **Keycloak** for all authentication. Every API request requires a valid JWT token, and each organization maps to a dedicated Keycloak realm. This page covers OIDC flows, token management, machine-to-machine authentication, and session configuration. ## Authentication Architecture Browser-based applications authenticate using the **OIDC Authorization Code flow with PKCE**: 1. The frontend redirects the user to the Keycloak realm login page 2. The user authenticates (password, SSO, or MFA) 3. Keycloak returns an authorization code to the frontend callback URL 4. The frontend exchanges the code for JWT tokens (access + refresh + ID) 5. The access token is included in all API requests as `Authorization: Bearer ` The PKCE extension protects against authorization code interception attacks in public clients (browser-based applications that cannot securely store a client secret). Service accounts, CI/CD pipelines, and automation tools authenticate using the **Keycloak client credentials grant**: ```bash theme={null} curl -X POST \ "https:///realms//protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=" \ -d "client_secret=" ``` Client credentials are managed entirely by Keycloak -- there is no custom API key lifecycle. Keycloak handles credential rotation, revocation, and expiration. ## JWT Token Structure Access tokens issued by Keycloak contain the following claims used by the platform: | Claim | Source | Description | | -------- | ------------------ | ---------------------------------------------------------- | | `sub` | Keycloak | Unique user identifier | | `iss` | Keycloak realm URL | Issuer URL including the realm ID | | `org_id` | Derived from realm | Organization ID (realm ID = org ID) | | `groups` | Keycloak groups | Group memberships for role mapping | | `aud` | Keycloak client | Audience -- identifies the intended recipient of the token | | `exp` | Keycloak | Token expiration timestamp | | `iat` | Keycloak | Token issuance timestamp | The `project_id` is never stored in the JWT. It is passed via the `X-Project-ID` HTTP header or as a path parameter on each API request. This allows a single token to access multiple projects within the same organization. ## Token Validation Platform services validate JWT tokens using the following process: The service fetches the Keycloak JWKS (JSON Web Key Set) from the realm's well-known endpoint: `https:///realms//protocol/openid-connect/certs` The token's RS256 signature is verified against the public key from the JWKS endpoint. The service validates the `iss` (issuer) and `exp` (expiration) claims. Expired or malformed tokens receive a `401 Unauthorized` response. Audience (`aud`) validation is not currently enforced; this is a known limitation tracked for remediation. The `org_id` is extracted from the realm portion of the issuer URL. All subsequent queries are scoped to this organization. ## Multi-Realm Architecture Each organization in the platform is a separate Keycloak realm: | Aspect | Implementation | | ------------------------ | ----------------------------------------------- | | **Realm ID** | Equals the organization ID | | **User directory** | Each realm has its own user store | | **IdP configuration** | SSO providers are configured per realm | | **Client registrations** | Applications and service accounts are per realm | | **Session management** | Sessions are isolated per realm | This architecture provides complete tenant isolation at the authentication level. A user authenticated in one realm cannot access resources in another realm's organization. ## Single Sign-On (SSO) Keycloak supports SSO via both OIDC and SAML 2.0 for enterprise identity providers: | IdP | Protocol | Configuration | | ------------------ | -------- | -------------------------------------- | | Microsoft Entra ID | OIDC | App registration + group claims | | Okta | OIDC | Application integration + group filter | | Google Workspace | OIDC | OAuth client credentials | | PingFederate | SAML 2.0 | SP metadata exchange | ### SSO Features * **Per-tenant configuration**: Each organization connects to its own IdP * **Just-in-time provisioning**: Users are created on first SSO login * **Forced SSO**: Organizations can disable password login, requiring all users to authenticate via the configured IdP * **Session management**: Configurable idle timeout, max session duration, and forced re-authentication See the [SSO Integration guide](/guides/sso-integration) for step-by-step configuration. ## Anonymous Access The platform supports optional anonymous read-only access for public resources: | Setting | Value | | ------------------ | ------------------------------------------------------------------------------------------------ | | **Scope** | Read-only access to PUBLIC entities via search and list endpoints | | **Rate limit** | 30 requests per minute (target; must be enforced at ingress/WAF layer by deploying organization) | | **Redaction** | Security schemes and credentials are omitted from responses | | **Default** | Disabled per tenant; must be explicitly enabled | | **Implementation** | `OptionalAuth` dependency returns `None` for unauthenticated requests | Anonymous endpoints must be explicitly allowlisted in the service configuration. The standard `get_auth()` dependency always requires a valid JWT. ## Rate Limiting Rate limiting must be enforced at the ingress/WAF layer by the deploying organization. The following targets are recommended: | Operation Type | Recommended Limit | Enforcement | | --------------------------------- | ----------------- | ------------------ | | Writes (POST, PUT, PATCH, DELETE) | 60/min | Ingress/WAF policy | | Reads (GET, POST :search) | 300/min | Ingress/WAF policy | | Bulk operations | 10/min | Ingress/WAF policy | | Anonymous reads | 30/min | Ingress/WAF policy | When rate limiting is configured, the ingress layer should return `429 Too Many Requests` with a `Retry-After` header and include standard rate limit response headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`). Rate limiting is not enforced by default in the platform. Deploying organizations must configure rate limiting at their ingress controller or WAF (e.g., GCP Cloud Armor, AWS WAF, Nginx rate limiting). ## Token Management Best Practices Use the refresh token to obtain new access tokens before the access token expires. Keycloak refresh tokens have a longer lifetime than access tokens. Implement proactive refresh (e.g., refresh when 75% of the token lifetime has elapsed) rather than waiting for a 401 response. Rotate client secrets periodically. Keycloak supports multiple active client secrets during rotation, allowing a grace period for credential updates across services. Never store tokens in localStorage for browser applications. Use httpOnly cookies or in-memory storage with the token refresh flow. For server-side applications, store tokens in encrypted session state. ## Troubleshooting Check the token's `iss` claim matches the expected Keycloak realm URL. Verify the Keycloak server is reachable and the JWKS endpoint returns valid keys. Clock skew between services can cause premature token expiration. Verify the redirect URI configured in the IdP matches the Keycloak broker endpoint. Check that the Keycloak realm has the identity provider enabled and the browser flow is configured correctly. Ensure a group mapper is configured in the Keycloak client settings. The mapper must be set to add group memberships to the access token (not just the ID token). ## Next Steps Configure SSO with Entra ID, Okta, or Google Workspace. Set up OpenFGA roles and permissions for authorization. Learn how to authenticate API requests with JWT tokens. Review TLS and network policies for securing authentication flows. # GDPR Compliance Source: https://docs.emergence.ai/security/compliance/gdpr How CRAFT supports GDPR compliance with data handling, Right to Be Forgotten, audit logging, and data residency controls. # GDPR Compliance CRAFT is designed to support organizations in meeting their obligations under the **General Data Protection Regulation (GDPR)**. This page covers data handling, Right to Be Forgotten (RTBF) implementation, audit logging, and data residency controls. CRAFT provides the technical mechanisms for GDPR compliance. Organizations are responsible for configuring and operating the platform in accordance with their specific GDPR obligations, Data Protection Impact Assessments (DPIAs), and legal counsel. ## Data Processing Principles The platform implements GDPR's data processing principles at the architecture level: | Principle | Implementation | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Lawfulness** | Platform enforces authenticated access controls; deploying organizations must establish and document their Article 6 lawful basis (e.g., legitimate interests, contract performance, consent) for each processing activity in their Records of Processing Activities (RoPA) | | **Purpose limitation** | Data connections are scoped to specific projects with defined purposes | | **Data minimization** | Agent Cards apply data minimization to provider contact information | | **Accuracy** | Audit logs record all data modifications with timestamps | | **Storage limitation** | Configurable retention policies per tenant and data type | | **Integrity and confidentiality** | Encryption at rest (AES-256) and in transit (TLS 1.2 minimum) | | **Accountability** | Immutable audit logs with full traceability | ## Personal Data Inventory The platform processes the following categories of personal data: | Category | Location | Sensitivity | Purpose | Suggested Lawful Basis | | --------------------------- | ------------------ | ------------ | ------------------------ | ----------------------------------------------- | | User identity (email, name) | Keycloak realms | Restricted | Authentication | Contract performance or legitimate interests | | User IP address | Audit logs | Restricted | Security monitoring | Legitimate interests | | User agent string | Audit logs | Internal | Debugging | Legitimate interests | | Agent Card provider contact | Assets database | Confidential | Agent registration | Contract performance | | Session data | Redis, PostgreSQL | Confidential | Multi-turn conversations | Contract performance | | Query results | Solution databases | Confidential | Data analytics | Determined by organization based on data source | The "Suggested Lawful Basis" column provides guidance only. Organizations must confirm the lawful basis for each processing activity in their specific deployment context and document it in their Records of Processing Activities (RoPA). ## Right to Be Forgotten (RTBF) The platform implements RTBF using **crypto-shredding** -- destroying the encryption key rather than locating and deleting every instance of the data. ### How Crypto-Shredding Works When audit log entries are created, PII fields (`actor` identity, `ip_address`) are encrypted using a per-principal encryption key. An administrator or the data subject initiates an RTBF request via the Governance API. The principal's encryption key is permanently deleted. Without the key, encrypted fields become unrecoverable. The audit log entry structure is preserved (action, resource\_type, resource\_id, timestamp, trace\_id), but the `actor` field displays `[REDACTED]`. ### Data Deletion Matrix When an RTBF request is processed, data is deleted across all storage systems: | Storage | Action | Timing | | ----------------------------- | --------------------------------------------------------- | ---------------------------------------- | | **PostgreSQL** | Hard-delete rows from all tables | Immediate | | **OpenFGA** | Delete authorization relationship tuples | Immediate | | **Object storage** | Delete skill package artifacts | Immediate | | **Redis** | Invalidate cached entries | Immediate | | **Event bus** (Redis Streams) | Events are ephemeral (7-day retention) | No action needed beyond retention expiry | | **WAL/PITR backups** | Backup retention must not exceed RTBF compliance deadline | Per backup retention policy | | **Operational logs** | PII limited to tenant\_id and actor; 90-day max retention | Retention-based expiry | WAL and PITR backups may contain personal data. GDPR controllers have one month to respond to RTBF requests (extendable by two months for complex cases under Art. 12(3)). Recommended maximum backup retention is 30-90 days to stay within RTBF response obligations. The default infrastructure configuration retains 7 daily backups with 7-day transaction log retention. Long-term backups containing personal data require either crypto-shredding capability or documented justification under Art. 17(3) exemptions. ## Audit Logging Every state-changing operation produces an immutable audit entry: | Field | Type | Description | | --------------- | --------- | ---------------------------------------------------------- | | `id` | ULID | Unique entry identifier | | `tenant_id` | string | Tenant scope | | `action` | string | e.g., `agent.register`, `agent.approve`, `policy.update` | | `actor` | string | Authenticated principal (encrypted with per-principal key) | | `actor_type` | enum | USER, SERVICE\_ACCOUNT, SYSTEM | | `resource_type` | string | Target resource type | | `resource_id` | string | Target resource identifier | | `timestamp` | timestamp | When the action occurred | | `details` | JSONB | Old/new values, approval comments, policy violations | | `ip_address` | string | Client IP (encrypted with per-principal key) | | `trace_id` | string | OpenTelemetry trace ID for end-to-end correlation | ### Audit Log Properties * **Append-only**: No updates or deletes in normal operation * **Retention**: Configurable per tenant (default 1 year; extended retention only where required by specific regulation, e.g., financial record-keeping) * **Export**: `GET /governance/admin/audit-logs?format=csv` for compliance reporting * **SIEM integration**: Forward to Splunk, Datadog, or SIEM via OpenTelemetry Collector (customer-configurable exporters) * **Filterable**: By actor, action, date range, and resource ## Breach Notification GDPR Article 33 requires that in the event of a personal data breach, the controller must notify the competent supervisory authority without undue delay and, where feasible, no later than **72 hours** after becoming aware of it. Article 34 requires notification to affected data subjects where the breach is likely to result in a high risk to their rights and freedoms. The platform supports breach detection and timely reporting through: * **SIEM integration**: Real-time forwarding of security events via OpenTelemetry Collector for anomaly detection * **Audit log analysis**: Identify unauthorized access patterns via audit log queries and export * **OpenTelemetry alerting**: Configure alerts for unusual access patterns (high volume queries, off-hours access, privilege escalation) * **Trace correlation**: `trace_id` in audit logs enables end-to-end investigation of suspected breaches Organizations must establish their own breach notification procedures, including designated contacts for supervisory authority notification, internal escalation chains, and data subject communication templates. The platform provides detection and evidence tools, not the notification process itself. ## Data Residency For GDPR Article 44-49 compliance (international data transfers): * **Region labels**: Each tenant is tagged with a data residency region (EU, US, APAC, custom) * **Geo-fenced storage**: Database partitioning by region ensures EU data stays in EU * **Regional API endpoints**: Optional per-region API gateway routing (e.g., `eu.platform.example.com`) * **Cross-region search**: Federated queries can span regions only with explicit opt-in * **Data location attestation**: API endpoint returns storage location metadata per entity ### Legal Transfer Mechanisms Technical geo-fencing alone does not constitute GDPR-compliant transfer controls. Cross-border data flows require a valid legal transfer mechanism under GDPR Chapter V: * **Standard Contractual Clauses (SCCs)** (Art. 46(2)(c)) -- the most common mechanism for international transfers * **Adequacy decisions** (Art. 45) -- transfers to countries with an adequate level of data protection * **Binding Corporate Rules** (Art. 47) -- for intra-group international transfers Organizations should review their Data Processing Agreements (DPAs) with cloud providers to ensure EU-standard protections for transferred data, including appropriate SCCs. ## Consent and Access Controls | GDPR Right | Platform Mechanism | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Right of access** (Art. 15) | Audit logs and data export APIs. Organizations must respond to DSARs within one month of receipt (Art. 12(3)), extendable by two months for complex requests | | **Right to rectification** (Art. 16) | Standard CRUD APIs for data correction | | **Right to erasure** (Art. 17) | Crypto-shredding + data deletion matrix | | **Right to restrict processing** (Art. 18) | Tenant-level freeze / per-resource access controls | | **Right to data portability** (Art. 20) | JSON/CSV export APIs | | **Right to object** (Art. 21) | Organizations must implement a process to receive and act on individual objection requests. Platform supports per-user processing restrictions via access controls. Tenant-level processing settings provide additional scope controls | ## Data Protection Impact Assessment (DPIA) When deploying the platform for processing personal data, consider the following for your DPIA: Map the flow of personal data through the platform: authentication tokens, audit logs, data connections, query results, and agent interactions. Identify which components process personal data and the legal basis for each. Configure the platform to collect only the data necessary for your use case. Disable anonymous access logging if not needed. Configure audit log retention to the minimum required period. If using cloud-managed services (Cloud SQL, RDS, Azure Database), ensure your Data Processing Agreement (DPA) with the cloud provider covers the data being processed. If using cloud-based LLM APIs, a DPA with the LLM provider is required. A formal DPIA is mandatory under Art. 35 for processing likely to result in high risk, including: systematic profiling, large-scale processing of sensitive data (e.g., healthcare via Data Insights), and automated decision-making. Engage your Data Protection Officer (DPO) per Art. 37 in reviewing the DPIA, and consult your supervisory authority under Art. 36 if residual risk remains high after mitigation. ## Compliance Reporting The platform provides compliance reports via the admin API: | Report | Content | | ----------------------------- | --------------------------------------------------------- | | **Data processing inventory** | All data connections, their purposes, and access patterns | | **Access review** | All users and their permissions per tenant | | **Audit trail export** | Full audit log for a specified date range | | **RTBF confirmation** | Confirmation of data deletion across all storage systems | ## Next Steps See how the platform maps to SOC 2 Trust Service Criteria. Review HIPAA compliance considerations for healthcare deployments. Understand data classification levels and encryption requirements. Configure backup retention to align with GDPR requirements. # HIPAA Compliance Source: https://docs.emergence.ai/security/compliance/hipaa HIPAA compliance considerations for deploying CRAFT in healthcare environments with PHI data handling requirements. # HIPAA Compliance This page covers considerations for deploying CRAFT in healthcare environments where **Protected Health Information (PHI)** may be processed. The platform's security architecture provides the technical controls needed to support HIPAA compliance when properly configured. HIPAA compliance is a shared responsibility. The platform provides technical safeguards, but organizations must implement administrative safeguards (policies, training, BAAs) and physical safeguards appropriate to their deployment model. Consult with your compliance team and legal counsel. ## HIPAA Applicability HIPAA may apply when CRAFT is used to: * Query healthcare databases containing patient records via Data Insights * Profile and govern healthcare data assets via Data Governance * Process data from Electronic Health Records (EHR) systems * Generate analytics or visualizations from clinical data In these scenarios, the platform acts as a **Business Associate** processing PHI on behalf of the Covered Entity (healthcare organization). ## HIPAA Safeguards Mapping ### Technical Safeguards (45 CFR 164.312) | HIPAA Requirement | Section | Platform Control | | ------------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Access control** | 164.312(a)(1) | Keycloak multi-realm authentication with OIDC; OpenFGA ReBAC with role-based permissions | | **Unique user identification** | 164.312(a)(2)(i) | UUID-based user identifiers in Keycloak; JWT `sub` claim for audit attribution | | **Emergency access procedure** | 164.312(a)(2)(ii) | Admin override capabilities; Keycloak emergency access accounts. Organizations must document emergency access procedures including: invocation criteria, authorization chain, mandatory post-use audit review, and account lifecycle controls | | **Automatic logoff** | 164.312(a)(2)(iii) | Session idle timeout and max duration configurable via Keycloak realm settings (Keycloak defaults: 30 min idle, 10 hr max). HIPAA deployments should configure per Minimum Viable HIPAA Configuration (15 min idle, 8 hr max recommended) | | **Encryption and decryption** | 164.312(a)(2)(iv) | AES-256 encryption at rest; TLS 1.3 for external, TLS 1.2 minimum for internal | | **Audit controls** | 164.312(b) | Immutable audit logs with ULID identifiers, configurable retention | | **Integrity controls** | 164.312(c)(1) | Parameterized SQL queries; schema validation; Pydantic request/response validation | | **Authentication** | 164.312(d) | Multi-factor authentication via Keycloak; SSO with enterprise IdPs | | **Transmission security** | 164.312(e)(1) | TLS 1.3 for all external communication; TLS 1.2 minimum for internal. mTLS available via service mesh | ### Administrative Safeguards (45 CFR 164.308) These require organizational implementation supported by platform capabilities: | HIPAA Requirement | Section | Platform Support | | --------------------------------- | ------------- | ------------------------------------------------------------------------------- | | **Security management process** | 164.308(a)(1) | Audit logs for risk assessment; vulnerability scanning in CI/CD | | **Workforce security** | 164.308(a)(3) | OpenFGA role assignments; SCIM 2.0 deprovisioning; access reviews | | **Information access management** | 164.308(a)(4) | Project-level access isolation; role-based computed permissions | | **Security awareness training** | 164.308(a)(5) | Organization responsibility (not platform-provided) | | **Security incident procedures** | 164.308(a)(6) | OpenTelemetry trace correlation; SIEM integration via OpenTelemetry Collector | | **Contingency plan** | 164.308(a)(7) | Backup/restore procedures; disaster recovery checklist; multi-zone availability | | **Evaluation** | 164.308(a)(8) | Periodic access reviews; compliance reporting APIs | | **BAA with subcontractors** | 164.308(b)(1) | Required with cloud providers when using managed services | ### Physical Safeguards (45 CFR 164.310) Physical safeguards are primarily the responsibility of the deployment environment: | HIPAA Requirement | Platform Relevance | | ----------------------------- | ------------------------------------------------------------------- | | **Facility access controls** | Cloud provider data center security (GCP, AWS, Azure SOC 2 reports) | | **Workstation use/security** | Organization responsibility for client devices | | **Device and media controls** | Encryption at rest; crypto-shredding for data disposal | ## PHI Data Flow Controls When the platform processes PHI, apply these additional controls: ### Data Connection Security Create a dedicated read-only database user for PHI data connections. Grant access only to the specific tables and schemas needed. Never grant write access unless explicitly required. Set `ssl_mode: verify-full` on all data connections to PHI databases. This ensures encrypted connections with server identity verification. Use project-level isolation to restrict which teams can query PHI databases. Assign the `viewer` role to analytics users who should only read query results, and `developer` to those who configure data connections. HIPAA requires 6-year retention for policies, procedures, and documentation (45 CFR 164.530(j)(2)). Audit log retention is a separate organizational decision based on risk assessment. Configure `retention_years: 7` as a recommended default to cover both documentation and audit trail needs. ### Data Minimization | Control | Implementation | | ---------------------- | ----------------------------------------------------------------------------------------------------- | | Query result retention | Configure session TTL to minimize PHI retention in platform databases | | Cache expiration | Set Redis TTL to the minimum needed for session continuity | | Log redaction | Ensure PHI is not logged in operational logs; limit to tenant\_id and actor | | Profiling scope | When profiling PHI databases, limit profiling to metadata (schema, statistics) rather than raw values | ## Minimum Viable HIPAA Configuration For healthcare deployments, apply these configuration overrides: ```yaml theme={null} # Helm values for HIPAA-compliant deployment security: tls: minVersion: "1.3" # External traffic; internal defaults to 1.2 minimum enforceExternal: true # enforceInternal: true # Requires service mesh for internal TLS 1.3 keycloak: sessions: idleTimeout: "15m" # HIPAA automatic logoff (Keycloak default: 30m) maxLifespan: "8h" # Keycloak default: 10h mfa: enabled: true policy: "required" # Require MFA for all users audit: retentionYears: 7 # Recommended: covers HIPAA 6-year documentation retention + buffer piiEncryption: true # SIEM integration: configure OpenTelemetry Collector exporters per your SIEM provider # otelCollector: # exporters: # syslog: { ... } # Customer-configurable; not pre-configured dataConnections: sslMode: "verify-full" # Enforce verified SSL readOnly: true # Default to read-only connections ``` ## Business Associate Agreement (BAA) **Critical for Data Insights deployments:** If the platform sends PHI to a cloud-based LLM API (e.g., for natural-language analysis or metadata enrichment), a BAA with the LLM provider is **required**. This is a HIPAA violation without a BAA. Self-hosted LLMs (configurable via `api_base`) eliminate this requirement. When using cloud-managed services with PHI, ensure BAAs are in place: | Service | BAA Required With | Notes | | -------------------------- | ------------------------------- | ----------------------------------------------------------------------- | | Cloud SQL / RDS / Azure DB | Cloud provider | Covers PHI stored in managed databases | | Memorystore / ElastiCache | Cloud provider | If PHI is cached in Redis | | GCS / S3 / Azure Blob | Cloud provider | If PHI-derived artifacts are stored | | Secrets backend | Infisical or GCP Secret Manager | If using hosted secrets management | | LLM providers | LLM vendor | **Required** if PHI is sent to LLM APIs for analysis. See warning above | ## Breach Notification HIPAA requires notification to affected individuals within 60 calendar days of **discovery** of the breach (45 CFR 164.404(b)). Additionally, notification to the Secretary of HHS is required (45 CFR 164.408), and notification to prominent media outlets may be required for breaches affecting more than 500 residents of a state (45 CFR 164.406). The platform supports breach detection through: * **Audit log analysis**: Identify unauthorized access patterns via audit log queries * **SIEM integration**: Real-time forwarding of security events for anomaly detection * **OpenTelemetry alerting**: Configure alerts for unusual access patterns (high volume queries, off-hours access, privilege escalation) ## Next Steps Review GDPR controls that complement HIPAA requirements. See the full SOC 2 Trust Service Criteria mapping. Understand data classification levels for PHI handling. Configure MFA and session management for HIPAA compliance. # SOC 2 Controls Source: https://docs.emergence.ai/security/compliance/soc2 SOC 2 Trust Service Criteria mapping for CRAFT, covering security, availability, processing integrity, confidentiality, and privacy. # SOC 2 Controls Mapping This page maps CRAFT's security controls to the **SOC 2 Trust Service Criteria (TSC) 2017** (revised 2022) defined by the AICPA. CRAFT is the core platform powering the Emergence product suite. The mapping covers all five trust service categories: Security, Availability, Processing Integrity, Confidentiality, and Privacy. This mapping documents the platform's built-in controls. Organizations pursuing SOC 2 Type II certification must also implement organizational controls (policies, procedures, training) and engage a qualified auditor. ## Security (Common Criteria) Security controls form the foundation of all SOC 2 categories. ### CC6: Logical and Physical Access Controls | Criteria | Control | Platform Implementation | | -------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CC6.1 | Logical access security | Keycloak multi-realm JWT authentication with OIDC/PKCE | | CC6.2 | User registration and authorization | OpenFGA ReBAC with role-based permissions (owner, admin, member, developer, operator, viewer) | | CC6.3 | Access removal | Keycloak user deactivation + OpenFGA relation cleanup; SCIM 2.0 deprovisioning from IdP | | CC6.6 | Boundary protection | Kubernetes NetworkPolicies, ingress TLS termination, SSRF protection | | CC6.7 | Data transmission protection | TLS 1.3 for external traffic, TLS 1.2 minimum for internal (due to managed service compatibility -- Cloud SQL, Redis, Keycloak). mTLS available via service mesh (Istio, Linkerd) | | CC6.8 | Unauthorized software prevention | Container-based deployment, immutable images, no SSH access to pods | ### CC7: System Operations | Criteria | Control | Platform Implementation | | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | CC7.1 | Detection of vulnerabilities | Container image scanning in CI/CD, dependency pinning with `==` | | CC7.2 | Anomaly detection | OpenTelemetry traces/metrics/logs with Grafana LGTM alerting | | CC7.3 | Security incident evaluation | Immutable audit logs with SIEM integration via OpenTelemetry Collector (customer-configurable exporters) | | CC7.4 | Incident response | Audit trail with trace\_id correlation for investigation; configurable alerting thresholds for detection. Organizations must maintain a documented IR plan with roles and escalation paths | ### CC8: Change Management | Criteria | Control | Platform Implementation | | -------- | ------------------------ | ---------------------------------------------------------------------- | | CC8.1 | Change authorization | GitHub PR reviews, changeset-based versioning, CI/CD gates | | CC8.2 | Infrastructure changes | Terraform IaC with plan/apply workflow, ArgoCD GitOps | | CC8.3 | Configuration management | Helm values files, immutable Alembic migrations, version-driven builds | ## Availability | Criteria | Control | Platform Implementation | | -------- | ------------------------- | ----------------------------------------------------------------------------------- | | A1.1 | Processing capacity | Kubernetes autoscaling (GKE Autopilot, HPA), Redis connection pooling | | A1.2 | Recovery from disruptions | Multi-database backup strategy, point-in-time recovery, disaster recovery checklist | | A1.3 | Environmental protections | Cloud provider infrastructure (GKE, EKS, AKS) with multi-zone availability | ### Backup and Recovery Controls | Control | Implementation | | ---------------------- | ----------------------------------------------------------------------------------------------- | | Automated backups | Cloud-managed daily backups with configurable retention (30 days default) | | Point-in-time recovery | WAL archiving for PostgreSQL PITR | | Backup testing | Regular restore validation as part of DR procedures (frequency defined in organization DR plan) | | Startup order | Documented service dependency DAG for orderly recovery | ## Processing Integrity | Criteria | Control | Platform Implementation | | -------- | --------------------- | ---------------------------------------------------------------------------- | | PI1.1 | Processing accuracy | SQLAlchemy ORM with strict typing, Pydantic request/response validation | | PI1.2 | Input validation | FastAPI automatic request validation, search query injection prevention | | PI1.3 | Processing monitoring | Prefect workflow orchestration with status tracking, health check monitoring | | PI1.4 | Output completeness | Auto-generated SDKs ensure API contract compliance, E2E test coverage | ### Data Integrity Controls | Control | Implementation | | -------------------- | -------------------------------------------------------------------------------------- | | Schema enforcement | SQLAlchemy 2.0+ with strict type annotations, Alembic migration validation | | API contract testing | Auto-generated Python/TypeScript SDKs from OpenAPI specs, E2E tests via generated SDKs | | Query safety | `plainto_tsquery()` for user input, parameterized queries, no string interpolation | | Webhook integrity | HMAC-SHA256 signed payloads with unique delivery IDs (planned -- not yet released) | ## Confidentiality | Criteria | Control | Platform Implementation | | -------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | C1.1 | Confidential information identification | Four-level data classification (Public, Internal, Confidential, Restricted). Query result sensitivity inherits from the underlying data source; organizations must classify their data connections appropriately | | C1.2 | Confidential information disposal | Crypto-shredding for RTBF, configurable retention policies, backup rotation | Query result sensitivity inherits from the underlying data source. Healthcare data may be Restricted, while business analytics may be Confidential. Organizations must classify their data connections appropriately. See [Data Classification](/security/data-classification) for the full taxonomy. ### Encryption Controls | Control | Implementation | | --------------------- | -------------------------------------------------------------------------------------------- | | Encryption at rest | AES-256 via cloud-managed encryption, CMEK support | | Encryption in transit | TLS 1.3 (external), TLS 1.2 minimum (internal). mTLS available via service mesh | | Key management | Cloud KMS or provider-managed keys, per-principal keys for audit PII | | Secrets management | Platform Secrets API (Infisical or ESO + GCP Secret Manager), never stored in code or config | ## Privacy | Criteria | Control | Platform Implementation | | -------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | P1.1 | Privacy notice | Data processing purposes documented per data connection | | P2.1 | Choice and consent | Configurable per-tenant data processing settings | | P3.1 | Personal information collection | Minimal PII collection (email, IP for audit); data minimization applied | | P4.1 | Use, retention, and disposal | Configurable retention per data type, automated cleanup | | P5.1 | Access to personal information | Audit log export, data portability APIs (JSON/CSV) | | P6.1 | Disclosure to third parties | No user data shared by default. LLM-powered features (when enabled) send user queries and schema context to the configured LLM provider; DPA/BAA required. Cloud provider DPAs for managed services. Self-hosted LLM option eliminates third-party data transfer | | P7.1 | Quality of personal information | User profile sync via SCIM, Keycloak-managed identity | | P8.1 | Complaints handling | Admin audit trail for RTBF request tracking | ## Access Review Controls The platform supports periodic access reviews for SOC 2 compliance: Administrators can review all RBAC grants per tenant using the Governance API. The review includes role assignments, last login timestamps, and permission inheritance paths. Automated detection of agent registrations whose publisher account has been deactivated. Orphaned resources are flagged for review or ownership transfer. Entities with no updates and consecutive failing health checks for a configurable number of days are flagged for review. Stale entities may indicate abandoned resources or decommissioned services. When users are deactivated in the enterprise IdP, SCIM 2.0 sync automatically removes their Keycloak realm access and cleans up OpenFGA relations. ## Evidence Collection For SOC 2 audit evidence, the platform provides: | Evidence | Source | Export Format | | ------------------- | -------------------------------- | ----------------- | | Access logs | Audit log API | JSON, CSV | | Change history | Git commits, ArgoCD sync history | Git log, API | | Configuration state | Helm values, Terraform state | YAML, JSON | | Vulnerability scans | CI/CD pipeline artifacts | SARIF, JSON | | Backup verification | Cloud provider backup logs | Provider-specific | | Incident records | Alert history, trace correlation | JSON | ## Next Steps Learn about GDPR-specific controls and Right to Be Forgotten. Review HIPAA compliance considerations for healthcare deployments. Deep dive into the authentication architecture. Review data classification levels and encryption standards. # Data Classification Source: https://docs.emergence.ai/security/data-classification Data classification levels, encryption at rest and in transit, and data handling requirements across CRAFT. # Data Classification CRAFT classifies all data processed by the platform into defined sensitivity levels with corresponding handling requirements. This page covers the classification taxonomy, encryption standards, and data handling policies. ## Classification Levels **Level 0 -- Public** Data intended for public consumption. No access control required. | Attribute | Value | | ------------------------- | ------------------------------------------------------ | | **Access** | No authentication required | | **Encryption at rest** | Inherits from platform default (cloud-managed AES-256) | | **Encryption in transit** | Recommended (TLS) | | **Retention** | Per content policy | **Examples:** Public Agent Cards, public documentation, published API specifications, marketplace listing descriptions. **Level 1 -- Internal** Data intended for internal use within an organization. Accessible to all authenticated members. | Attribute | Value | | ------------------------- | ---------------------------------------------- | | **Access** | Authenticated users with `can_read` permission | | **Encryption at rest** | Required | | **Encryption in transit** | Required (TLS 1.2 minimum) | | **Retention** | Per organization policy | **Examples:** Agent metadata, project configurations, workflow definitions, health check results, non-sensitive audit log entries. **Level 2 -- Confidential** Sensitive business data requiring restricted access. Disclosure could cause significant business impact. | Attribute | Value | | ------------------------- | -------------------------------------------------------------- | | **Access** | Role-based, minimum `developer` or `operator` role | | **Encryption at rest** | Required (AES-256) | | **Encryption in transit** | Required (TLS 1.2 minimum; TLS 1.3 where supported by service) | | **Retention** | Per data governance policy, with right-to-erasure support | **Examples:** Customer database contents accessed via data connections, query results, data profiling output, enrichment metadata, analytics insights. **Level 3 -- Restricted** Highly sensitive data. Unauthorized disclosure could cause severe business, legal, or regulatory impact. | Attribute | Value | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Access** | `admin` or `owner` role, with audit logging | | **Encryption at rest** | Required (AES-256, CMEK where available) | | **Encryption in transit** | Required (TLS 1.2 minimum for all traffic). mTLS for internal services requires service mesh deployment (e.g., Istio, Linkerd) -- not included by default | | **Retention** | Regulatory minimum, with crypto-shredding for erasure | **Examples:** Data connection credentials, API secrets, Keycloak realm configurations, HMAC webhook secrets, user PII (email, IP address), audit log actor identities. ## Encryption at Rest All persistent data is encrypted at rest using industry-standard encryption: | Component | Encryption Method | Key Management | | ------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | **PostgreSQL** | Cloud-managed encryption (AES-256) | Google-managed or CMEK (Cloud SQL), AWS KMS (RDS), Azure-managed (Flexible Server) | | **Redis** | Cloud-managed encryption where available | Provider-managed keys | | **Secrets backend** | Application-level secrets: Infisical (envelope encryption) or GCP Secret Manager (KMS-managed) | Backend-specific key hierarchy | | **Object storage** | Server-side encryption (AES-256) | Provider-managed or CMEK | | **Webhook secrets** | Envelope encryption | Per-subscription encryption keys | | **Audit log PII** | Per-principal encryption keys | Enables crypto-shredding for RTBF | ### Customer-Managed Encryption Keys (CMEK) For organizations with strict key management requirements, the platform supports CMEK on cloud-managed services: Use Cloud KMS keys with Cloud SQL and GCS. Configure via Terraform: ```hcl theme={null} resource "google_sql_database_instance" "main" { encryption_key_name = google_kms_crypto_key.database.id } ``` Use AWS KMS keys with RDS and S3. Configure via Terraform with `kms_key_id` parameters. Use Azure Key Vault keys with Azure Database for PostgreSQL. Configure via Terraform with customer-managed key references. ## Encryption in Transit All network communication is encrypted using TLS: | Channel | Minimum TLS Version | Notes | | --------------------------- | ------------------- | -------------------------------------------------- | | External API traffic | TLS 1.3 | Mandatory for all client-to-platform communication | | Internal service-to-service | TLS 1.2 | TLS 1.3 recommended | | Database connections | TLS 1.2 | `sslmode=require` or higher | | Redis connections | TLS 1.2 | In-transit encryption enabled | | Keycloak to IdP | TLS 1.2 | Required for OIDC and SAML flows | | Webhook deliveries | TLS 1.2 | HTTPS required in production environments | ### Mutual TLS (mTLS) For zero-trust Kubernetes deployments, the platform supports mTLS for internal service communication via a service mesh (Istio, Linkerd). ## Data Handling by Component | Component | Data Handled | Classification | Special Handling | | ------------------- | ------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------- | | **Governance** | Organizations, users, roles | Internal - Restricted | PII in audit logs encrypted per-principal | | **Assets** | Artifacts, data connections, files, models | Internal - Restricted | Credentials stored via platform Secrets API (Infisical or ESO), never in DB | | **Utils** | Data catalog, scheduling, context packs, memories | Internal - Confidential | Memory content encrypted at rest; context pack metadata access-controlled | | **Data Insights** | Query results, SQL, visualizations | Confidential | Customer data accessed read-only via connections | | **Data Governance** | Profiles, metadata, DQ rules | Confidential | Metadata derived from customer data | | **Keycloak** | User identities, sessions | Restricted | Realm isolation, password hashing | | **OpenFGA** | Authorization tuples | Internal | No PII, relationship data only | ## Data Residency For organizations with data sovereignty requirements: * **Region labels**: Each tenant is tagged with a data residency region (EU, US, APAC, custom) * **Geo-fenced storage**: Database partitioning by region ensures data stays within designated boundaries * **Regional endpoints**: Optional per-region API routing (e.g., `eu.platform.example.com`) * **Cross-region search**: Federated queries can span regions with explicit opt-in; results indicate source region ## Data Retention | Data Type | Default Retention | Regulatory Override | | ------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Audit logs | 1 year | Extended retention where required by specific regulation (e.g., financial record-keeping). Note: HIPAA's 6-year retention (45 CFR 164.530(j)(2)) applies to policies and documentation, not specifically to audit logs | | Operational logs | 90 days | -- | | Session data | 30 days after last activity | -- | | Webhook delivery logs | 30 days | -- | | Data profiling results | Organization-defined (recommend aligning with data connection retention) | Per GDPR RTBF; storage limitation principle requires justification for extended retention | | Event bus (Redis Streams) | 7 days | -- | ## Next Steps Learn about GDPR data handling, Right to Be Forgotten, and audit requirements. Review TLS configuration, SSRF protection, and network policies. See how data classification maps to SOC 2 Trust Service Criteria. Understand backup encryption and retention for disaster recovery. # Network Security Source: https://docs.emergence.ai/security/network-security Network policies, SSRF protection, TLS configuration, and network segmentation for CRAFT. # Network Security CRAFT implements defense-in-depth network security including TLS encryption, SSRF protection, Kubernetes network policies, and DNS security. This page covers the network security architecture and configuration. ## TLS Configuration All external and internal communication is encrypted with TLS: | Channel | Protocol | Minimum Version | Certificate Management | | --------------------- | -------- | --------------- | ----------------------------------------------------------------- | | Client to API gateway | HTTPS | TLS 1.3 | Managed certificates (Let's Encrypt, cloud-managed, or custom CA) | | Service to service | HTTPS | TLS 1.2 | Internal CA or service mesh | | Service to PostgreSQL | TLS | TLS 1.2 | Cloud-managed or custom CA | | Service to Redis | TLS | TLS 1.2 | Cloud-managed | | Keycloak to IdP | HTTPS | TLS 1.2 | IdP-managed certificates | | Webhook deliveries | HTTPS | TLS 1.2 | Target server certificates | TLS 1.0 and 1.1 are not supported. The platform enforces TLS 1.2 as a minimum and recommends TLS 1.3 for all external connections. ## SSRF Protection All outbound HTTP requests are protected against Server-Side Request Forgery (SSRF) attacks. This includes JWS `jku` fetch, health checks, webhook delivery (planned), and crawler operations. ### Protection Layers | Protection | Implementation | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Private IP blocking** | Resolved IPs validated against RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), RFC 4193 (fc00::/7), link-local (169.254.0.0/16, fe80::/10), and loopback (127.0.0.0/8, ::1) | | **DNS pinning** | DNS resolution performed before connection; resolved IP pinned to prevent TOCTOU rebinding attacks | | **HTTPS enforcement** | `jku` URLs must use HTTPS; webhook targets must use HTTPS in production (when implemented) | | **Redirect limiting** | At most 2 redirects followed, with IP re-validation after each redirect | | **Domain allowlists** | Configurable per feature (e.g., `jku` domains, crawl domains) | | **Response size limits** | Crawler responses limited to 1 MB | | **Timeout enforcement** | `jku` fetch: 5 seconds; health checks: 10 seconds (configurable) | ### Per-Context Behavior Different outbound request types apply SSRF rules differently: | Context | Private IPs | SSRF Check | Notes | | ---------------- | ------------------------ | ---------- | ----------------------------------------------------------------------- | | Health checks | Skip with UNKNOWN status | Yes | Cards with private-IP endpoints are accepted; health checks are skipped | | JWS `jku` fetch | Reject | Yes | HTTPS required; domain allowlist applies | | Crawler | Reject | Yes | Admin-configured domains only | | Webhook delivery | Reject | Yes | HTTPS required in production; fire-and-forget | ## Kubernetes Network Policies The platform uses Kubernetes NetworkPolicies to restrict pod-to-pod communication: ```yaml theme={null} # Example: Governance service network policy apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: governance-netpol spec: podSelector: matchLabels: app: governance policyTypes: - Ingress - Egress ingress: - from: - podSelector: matchLabels: app: assets - podSelector: matchLabels: app: utils ports: - port: 8000 egress: - to: - podSelector: matchLabels: app: keycloak ports: - port: 8080 - to: - podSelector: matchLabels: app: openfga ports: - port: 8080 - to: - podSelector: matchLabels: app: postgresql ports: - port: 5432 ``` ### Network Segmentation The platform services are segmented into security zones: Receives traffic from external clients via the ingress controller or API gateway. Only the API endpoints and Keycloak login pages are exposed externally. Contains the platform services (Governance, Assets, Utils) and solution services (Data Insights, Data Governance). Services communicate via internal cluster networking with TLS. Contains PostgreSQL, Redis, and other data stores. Only accessible from the Application Zone. No direct external access is permitted. Contains Keycloak and OpenFGA. Keycloak is partially exposed (login pages) while OpenFGA is internal-only. ## Ingress Security ### Ingress Controller Configuration The platform's ingress controller (NGINX or cloud-managed) enforces: | Setting | Value | Purpose | | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------- | | `ssl-redirect` | `true` | Force HTTPS for all connections | | `hsts-max-age` | `31536000` | HTTP Strict Transport Security (1 year) | | `hsts-include-subdomains` | `true` | Apply HSTS to all subdomains | | `proxy-body-size` | `50m` | Limit request body size (matches skill package upload limit) | | `rate-limiting` | Customer-configured | Per-route rate limiting at the ingress level (must be configured by deploying organization) | ### Web Application Firewall (WAF) WAF protection is recommended for all production deployments, especially those processing PHI, financial data, or sensitive enterprise data: * **GCP**: Cloud Armor security policies with OWASP Top 10 managed rules * **AWS**: AWS WAF with AWS Managed Rules (Core Rule Set, Known Bad Inputs) * **Azure**: Azure Front Door or Application Gateway WAF with OWASP 3.2 ruleset Deployments without WAF should document this as an accepted risk in their security posture. At minimum, enable the cloud provider's managed OWASP Top 10 rule group and IP reputation-based blocking. ## DNS Security | Protection | Description | | -------------------- | ----------------------------------------------------------- | | **DNSSEC** | Recommended for external DNS zones to prevent DNS spoofing | | **Internal DNS** | Kubernetes CoreDNS for service discovery within the cluster | | **DNS pinning** | SSRF protection pins resolved IPs to prevent DNS rebinding | | **TXT verification** | Agent domain verification via DNS TXT records | ## Service Mesh (Optional) For zero-trust deployments, the platform supports service mesh integration: * Automatic mTLS between all pods * Authorization policies for fine-grained access control * Traffic management and observability * PeerAuthentication for strict mTLS enforcement * Automatic mTLS with minimal configuration * Lightweight proxy with low resource overhead * Service profiles for per-route reliability settings ## Webhook Security When released, outbound webhook payloads will include security headers for integrity verification: | Header | Description | | --------------------- | ---------------------------------------- | | `X-Webhook-Id` | Unique delivery ID for idempotency | | `X-Webhook-Timestamp` | ISO 8601 timestamp for replay protection | | `X-Webhook-Signature` | HMAC-SHA256 signature of the payload | Receivers should: 1. Verify the HMAC-SHA256 signature using the shared secret 2. Reject payloads older than 5 minutes (replay protection) 3. Use the delivery ID for idempotent processing Webhook secrets support rotation with a 24-hour overlap period. During rotation, both the old and new secrets are valid for signature verification. ## Troubleshooting Check Kubernetes NetworkPolicies. Verify the source pod has egress rules allowing traffic to the destination pod on the correct port. Use `kubectl describe networkpolicy` to inspect active policies. Verify the target URL resolves to a public IP address. If the target is behind a private DNS, add the domain to the appropriate allowlist. Health checks for agents with private IPs return UNKNOWN status by design. Check certificate validity and chain completeness. Verify the server supports TLS 1.2 or higher. For internal services, ensure the internal CA certificate is trusted by all pods. ## Next Steps Review the authentication architecture and token management. Understand encryption requirements by data classification level. Configure network-related Helm values for your deployment. Infrastructure-level networking configuration for Kubernetes. # CRAFT Security Overview Source: https://docs.emergence.ai/security/overview CRAFT security architecture: Keycloak authentication, OpenFGA authorization, secrets management (ESO or Infisical), multi-tenant isolation, and OpenTelemetry observability. # Security Overview Security is embedded into every layer of CRAFT. The security model is built on four pillars: **authentication** (who you are), **authorization** (what you can do), **secrets management** (protecting credentials), and **multi-tenant isolation** (keeping organizations separate). ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR EXT["External Request"] TLS["TLS / Gateway"] AUTHN["Keycloak AuthN
JWT Validation"] AUTHZ["OpenFGA AuthZ
Permission Check"] HANDLER["Request Handler"] DB["PostgreSQL
org_id-scoped queries"] DATA["Data"] EXT --> TLS --> AUTHN --> AUTHZ --> HANDLER --> DB --> DATA style EXT fill:#CC79A7,stroke:#555555,color:#000 style TLS fill:#0072B2,stroke:#555555,color:#fff style AUTHN fill:#D55E00,stroke:#555555,color:#000 style AUTHZ fill:#D55E00,stroke:#555555,color:#000 style HANDLER fill:#E69F00,stroke:#555555,color:#000 style DB fill:#009E73,stroke:#555555,color:#000 style DATA fill:#009E73,stroke:#555555,color:#000 ``` ## Security Architecture **Technology:** Keycloak with multi-realm JWT and OIDC/PKCE Keycloak provides the identity layer for the entire platform. Each organization is a separate Keycloak **realm**, providing complete identity isolation between tenants. | Feature | Details | | ---------------------- | ------------------------------------------------------------- | | **Protocol** | OpenID Connect (OIDC) with PKCE for browser clients | | **Token format** | JWT containing `org_id` (from realm), groups, and user claims | | **Multi-realm** | Each organization = one Keycloak realm (realm ID = org ID) | | **SSO support** | Enterprise SSO via SAML or OIDC identity provider federation | | **Session management** | Token refresh, revocation, and session timeout policies | The `project_id` is never stored in the JWT. It is passed via the `X-Project-ID` HTTP header or as a path parameter, allowing users to switch project context without re-authenticating. **Technology:** OpenFGA with Relationship-Based Access Control (ReBAC) OpenFGA provides fine-grained authorization based on relationships between entities rather than static role assignments. This enables permission inheritance and computed permissions. ### Roles | Role | Scope | Description | | ----------- | ---------------------- | ---------------------------------------------------- | | `owner` | Organization | Full control over the organization and all resources | | `admin` | Organization / Project | Administrative access to manage users and settings | | `member` | Project | Standard access to project resources | | `developer` | Project | Create and modify agents, connections, and workflows | | `operator` | Project | Deploy, schedule, and monitor resources | | `viewer` | Project | Read-only access to all resources | ### Computed Permissions Permissions are **computed at query time** from role relationships, not statically assigned: | Permission | Derived From | | -------------------- | --------------------------------- | | `can_read` | All roles (viewer and above) | | `can_write` | member, developer, admin, owner | | `can_delete` | admin, owner | | `can_execute` | developer, operator, admin, owner | | `can_manage_secrets` | admin, owner | ### Permission Inheritance Permissions flow downward through the resource hierarchy: ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR Org["Organization"] --> Proj["Project"] --> Res["Resource
(Agent, Data Connection, etc.)"] classDef level fill:#0072B2,stroke:#555555,color:#fff class Org,Proj,Res level ``` A user who is an `admin` of an organization automatically has admin-level computed permissions on all projects and resources within it. Solutions must never implement their own permission checks. All authorization is delegated to the Governance service via the `require_permission()` decorator or SDK calls. If Governance is unavailable, all downstream services return 403.
The platform supports two secrets backends, choose based on your deployment environment. See [Secrets Management](/security/secrets-management) for a full comparison. | Layer | Options | Scope | Examples | | ------------------------- | ---------------------------------- | ---------------------------------------- | ------------------------------------------------------ | | **Application (cloud)** | ESO + GCP Secret Manager | Platform services, synced to K8s Secrets | Data connection credentials, API keys | | **Application (on-prem)** | Infisical | Platform services | Data connection credentials, API keys, encryption keys | | **Infrastructure** | GCP Secret Manager (via Terraform) | Terraform-managed resources | Database passwords, service account keys | **Key properties:** * Secrets are never exposed in API responses * Per-organization namespace isolation (Infisical) or IAM-scoped access (GCP SM) * Credentials are injected at runtime only when needed * Auto-rotation supported: ESO + Stakater Reloader (cloud), Infisical rotation policies (self-hosted) Never commit secrets or credentials to configuration files or source code. Use the platform Secrets API for application secrets and GCP Secret Manager or your cloud provider's equivalent for infrastructure secrets. Multi-tenancy provides complete isolation between organizations at every layer of the stack: | Layer | Isolation Mechanism | | ------------------ | ------------------------------------------------------------------------ | | **Authentication** | Separate Keycloak realm per organization | | **Authorization** | Per-organization OpenFGA relationship tuples | | **Data** | All database queries filtered by `org_id` from JWT | | **Secrets** | Per-organization isolation (Infisical namespaces or GCP SM IAM bindings) | | **Storage** | Tenant-scoped object storage paths | ### Security Invariants These rules are enforced across all platform services: 1. The `org_id` **always** comes from the authenticated JWT token, never from request parameters 2. **List operations** filter by both `org_id` (from JWT) AND `project_id` (from header) at the database level 3. **Write operations** stamp the `org_id` from the JWT onto new resources 4. **No cross-service foreign keys** exist between databases 5. A missing org\_id filter on a list query is treated as a **security vulnerability** Platform services run entirely within your infrastructure. External data egress depends on the integrations and model providers you configure -- for example, LLM calls via LiteLLM are sent to the provider you choose (Gemini, GPT, Claude, or a self-hosted model). You control what data leaves your environment.
## Dual Authorization Pattern The platform enforces authorization through a dual pattern that combines database filtering with OpenFGA checks: | Operation Type | Database Filter | OpenFGA Check | | ------------------------ | -------------------------------------- | ------------------------------------------------- | | **List operations** | Filter by `auth.org_id` + `project_id` | `require_permission()` for project access | | **Get-by-ID operations** | Not needed | `require_permission()` handles authorization | | **Create operations** | Stamp `auth.org_id` on resource | `require_permission()` for project write access | | **Delete operations** | Not needed | `require_permission()` for resource delete access | This dual pattern prevents two classes of vulnerabilities: (1) listing resources across organizations (database filter) and (2) accessing specific resources without permission (OpenFGA check). ## Authentication Flow The user authenticates via OIDC/PKCE flow against their organization's Keycloak realm. The browser receives a JWT token containing `org_id` and group memberships. The client includes the JWT in the `Authorization: Bearer ` header and the active project in the `X-Project-ID` header. The Governance service validates the JWT signature, expiry, and issuer. It extracts `org_id` from the realm claim. OpenFGA evaluates the user's relationship to the requested resource. Computed permissions are resolved at query time based on role inheritance. If authorized, the request proceeds with the authenticated context. Database queries are scoped to `org_id` and `project_id`. ## Network Security | Control | Implementation | | ---------------------- | -------------------------------------------------------------------------------------------------------------- | | **SSRF protection** | Webhook and data connection endpoints validate target URLs against allowlists | | **HMAC signatures** | Webhook payloads will be signed with HMAC for integrity verification (planned -- not yet released) | | **TLS** | All inter-service communication uses TLS in production | | **Private networking** | Cloud deployments use private VPC networking; self-managed clusters should use equivalent network segmentation | | **Firewall rules** | Terraform-managed ingress/egress rules | ## Observability and Audit The platform provides comprehensive observability for security monitoring: | Component | Technology | Purpose | | -------------- | --------------------- | ----------------------------------------------- | | **Traces** | OpenTelemetry + Tempo | Distributed request tracing across services | | **Metrics** | OpenTelemetry + Mimir | Service health, latency, and throughput metrics | | **Logs** | OpenTelemetry + Loki | Structured logging with correlation IDs | | **Dashboards** | Grafana | Visualization and alerting for all telemetry | All telemetry is exported via the **OTLP** (OpenTelemetry Protocol) standard -- a vendor-neutral protocol compatible with any OTLP-capable backend. You choose your observability stack; the Grafana LGTM bundle (Loki, Grafana, Tempo, Mimir) is included by default but never required. ## Security Best Practices * Enable TLS for all ingress and inter-service communication * Configure Keycloak password policies and brute-force detection * Set up regular secret rotation (Infisical rotation policies or ESO + Stakater Reloader) * Monitor OpenFGA permission check latency and error rates * Review audit logs for unauthorized access attempts * Use private networking for database and cache connections * Never implement permission checks directly -- always use `require_permission()` * Always use `auth.org_id` from JWT, never from request parameters * Never store secrets in configuration files or environment variables in source control * Use the Governance SDK for all authentication and authorization operations * Validate webhook signatures before processing events (when webhook feature is released) * Use parameterized queries to prevent SQL injection (enforced by SQLAlchemy ORM) ## Next Steps Deep dive into Keycloak configuration and OIDC flows. Understand data classification and handling policies. Learn about network-level security controls and firewall configuration. Step-by-step guide for configuring roles and permissions. # Secrets Management Source: https://docs.emergence.ai/security/secrets-management Overview of secrets management options for CRAFT, including ESO + GCP Secret Manager and Infisical as equal deployment alternatives. # Secrets Management CRAFT supports two secrets management backends. Both operate through the same **Governance Secrets API** abstraction, applications access secrets through a unified interface regardless of which backend is deployed. ## Secrets API Abstraction The Governance service exposes a Secrets API that proxies secret read/write operations to the configured backend. This decouples application code from the underlying secrets provider. ```text theme={null} Application → Governance Secrets API → [ ESO + GCP Secret Manager | Infisical ] ``` ## Option A: ESO + GCP Secret Manager **Recommended for cloud deployments on GCP.** Uses the [External Secrets Operator](https://external-secrets.io/) (ESO) to sync secrets from GCP Secret Manager into Kubernetes Secrets, with [Stakater Reloader](https://github.com/stakater/Reloader) for automatic pod restarts on secret changes. ### Architecture ```mermaid theme={null} %%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#555555', 'fontFamily': 'sans-serif', 'edgeLabelBackground': '#ffffff'}}}%% flowchart LR TF["Terraform
(GKE module)"] -->|provisions| SM["GCP Secret Manager"] SM -->|synced by| ESO["External Secrets
Operator"] ESO -->|creates| KS["Kubernetes Secrets"] KS -->|mounted in| Pod["Application Pod"] RLD["Stakater Reloader"] -->|watches| KS RLD -->|restarts| Pod classDef infra fill:#0072B2,stroke:#555555,color:#fff classDef process fill:#E69F00,stroke:#555555,color:#000 classDef data fill:#009E73,stroke:#555555,color:#000 classDef app fill:#56B4E9,stroke:#555555,color:#000 class TF,ESO,RLD infra class SM process class KS data class Pod app ``` ### Components | Component | Purpose | | ----------------------------- | ---------------------------------------------------------------------------- | | **GCP Secret Manager** | Cloud-native secret store, encrypted at rest, audit-logged, IAM-controlled | | **External Secrets Operator** | Kubernetes operator that syncs GCP SM secrets into K8s Secrets on a schedule | | **ClusterSecretStore** | Cluster-scoped ESO resource that configures the GCP SM connection | | **ExternalSecret** | Namespace-scoped resource mapping a GCP SM secret to a K8s Secret | | **Stakater Reloader** | Watches K8s Secrets for changes; triggers rolling restarts of annotated pods | ### Secret Naming Convention Secrets in GCP Secret Manager follow a `-` naming pattern. Specific names are environment-specific. ### Authentication ESO authenticates to GCP Secret Manager using [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity), no service account keys are stored in the cluster. ### Rotation When a secret value is updated in GCP Secret Manager: 1. ESO detects the change on its next sync interval (configurable, default 1h) 2. The K8s Secret is updated 3. Stakater Reloader detects the change (via annotation `reloader.stakater.com/auto: "true"`) and triggers a rolling restart For immediate rotation, manually trigger an ESO sync or restart the affected deployment. Secrets injected as environment variables require a pod restart to take effect, even after ESO syncs the updated value to the Kubernetes Secret. Stakater Reloader (included in reference infrastructure) automates this by triggering rolling restarts when annotated Secrets change. For security incidents, manually restart the affected deployment immediately after triggering an ESO sync. Consider reducing the default 1-hour sync interval for high-sensitivity secrets. ### Deployment See [Infrastructure Secrets](/deployment/infrastructure/secrets) for the step-by-step deployment guide. *** ## Option B: Infisical **Recommended for on-premises and cloud-agnostic deployments.** [Infisical](https://infisical.com/) provides application-level envelope encryption with a self-hostable server and native Kubernetes authentication. ### Architecture Infisical runs as a subchart within the em-runtime Helm release and stores secrets in its own PostgreSQL database (`infisical`). Services authenticate via Infisical's Kubernetes machine identity. ### Components | Component | Purpose | | ----------------------- | ----------------------------------------------------------------------- | | **Infisical server** | Self-hosted secret management platform (deployed as Helm subchart) | | **Infisical SDK** | Python/TypeScript SDK used by em-runtime services at startup | | **Machine Identity** | Kubernetes service account-based authentication (no static credentials) | | **Envelope encryption** | Secrets encrypted at rest with a per-secret encryption key hierarchy | ### Configuration Infisical is enabled by default in the em-runtime Helm chart: ```yaml theme={null} infisical: enabled: true resources: requests: cpu: "250m" memory: "512Mi" ``` To use an external Infisical instance instead of the subchart: ```yaml theme={null} infisical: enabled: false governance: extraEnvVars: - name: INFISICAL_URL value: "https://infisical.example.com" - name: INFISICAL_PROJECT_ID value: "your-project-id" ``` ### Rotation Infisical supports secret rotation via its dashboard or API. Service pods must be restarted to pick up new secret values (unlike ESO + Reloader which handles this automatically). Infisical requires manual pod restarts to pick up rotated secrets. For regulated deployments, configure Stakater Reloader (included in reference infrastructure) to watch Infisical-managed Kubernetes Secrets and trigger automated rolling restarts on change. Document the rotation procedure including verification steps. *** ## Comparison | Attribute | ESO + GCP Secret Manager | Infisical | | ---------------------- | ------------------------- | ---------------------------------- | | **Deployment model** | Cloud-native (GCP) | Self-hosted or cloud-agnostic | | **Cloud dependency** | Requires GCP | No cloud dependency | | **Encryption at rest** | GCP KMS (managed) | Envelope encryption (self-managed) | | **Auto-rotation** | Yes (Stakater Reloader) | Manual pod restart required | | **Audit logging** | GCP Cloud Audit Logs | Infisical audit logs | | **On-prem support** | No | Yes | | **Helm chart** | ESO Helm chart (separate) | Infisical subchart (included) | | **Auth mechanism** | GCP Workload Identity | Kubernetes machine identity | ## Choosing a Backend * **GKE (GCP cloud deployment)**: ESO + GCP Secret Manager is the default and is provisioned by Terraform * **On-premises or non-GCP cloud**: Infisical (included in Helm chart as subchart) * **Multi-cloud (EKS/AKS)**: Either, ESO supports AWS Secrets Manager and Azure Key Vault; Infisical is cloud-agnostic Both backends are supported and maintained. The choice depends on your deployment environment, not the platform version. ## Related Step-by-step guide to deploying ESO or Infisical in your cluster. How credentials are classified and protected within the platform. Helm values for enabling and configuring secrets backends. Network-level controls that protect secret access paths. # CRAFT Semiconductor Overview Source: https://docs.emergence.ai/semiconductor/overview Overview of the CRAFT semiconductor fab analytics platform for AI-assisted data analysis, workflow orchestration, and manufacturing insights. # Semiconductor Analytics The Semiconductor solution provides AI-assisted data analytics for semiconductor fabrication (fab) environments. It enables fab engineers to query manufacturing data using natural language, run automated analysis workflows, and gain insights into production processes, the AI agent generates and executes Python code on behalf of the engineer. ## Who Is It For Analyze manufacturing data, identify yield issues, and investigate process deviations using conversational AI instead of manual SQL queries. Monitor process parameters, detect drift, and correlate process variables with quality outcomes using AI-assisted analytics. Track defect rates, analyze wafer maps, and identify root causes of quality issues with automated data exploration. Access high-level dashboards and generate reports on fab performance, throughput, and equipment utilization. ## Key Capabilities Fab engineers interact with manufacturing data through a natural-language chat interface: * "What is the yield trend for product X over the past 30 days?" * "Which tools have the highest defect rates this week?" * "Compare process parameters between high-yield and low-yield lots" * "Show me wafer map defect patterns for the last 100 wafers" The system generates Python analysis code, executes it in the sandboxed Code Execution service using the platform's data-access layer, and returns results with interactive visualizations. Prefect-orchestrated workflows automate analysis and data management: * **Yield excursion detection**: Automated excursion detection with E1–E7 and parameter detectors, root cause analysis, and report generation (PDF, PPT, DOCX) * **Cross-product analysis**: Compare yield and excursion patterns across products * **Data ingestion**: Automated data extraction from fab systems (Optimal+) * **Feedback learning**: Daily policy refinement from user feedback The solution connects to standard semiconductor data sources: * Manufacturing Execution Systems (MES) * Equipment Data Collection (EDC) databases * Wafer test and sort databases * Inline metrology measurement databases * Defect inspection and review systems ## Architecture The Semiconductor solution follows the same architectural patterns as the other CRAFT solutions: | Component | Role | | ---------------------------- | -------------------------------------------------------------- | | **Conversational interface** | Natural-language queries against fab data | | **AI agents** | Pydantic AI agent with MCP tools for fab analytics | | **Code execution** | Sandboxed Python execution for data analysis and visualization | | **Workflow engine** | Prefect workflows for automated analysis pipelines | | **Platform integration** | Authentication, authorization, and data connections | ### Platform Integration The Semiconductor solution integrates with the shared CRAFT: * **Authentication**: Keycloak OIDC with organization-specific user management * **Authorization**: JWT-based access control with organization-scoped data queries * **Data connections**: Database connections managed via the data source connection system * **Multi-tenancy**: All data isolated per organization The Semiconductor solution operates as an independent platform with its own microservices (Backend API, AI Agent, MCP Server, Client Dashboard), shared library, and infrastructure. See the detailed architecture documentation for the full system design. ## Semiconductor-Specific Analytics | Analysis Type | Description | | ----------------------- | ---------------------------------------------------------------------------------------- | | **Yield analysis** | Lot-level and wafer-level yield calculation and trending | | **Excursion detection** | Automated detection of yield anomalies using E1–E7, WAT, WSP, and multivariate detectors | | **Process correlation** | Statistical correlation between process parameters and quality outcomes | | **Bin analysis** | Test bin distribution analysis and Pareto charts | | **Report generation** | Automated RCA and insight reports exportable as PDF, DOCX, and PPT | | **SPC monitoring** | Control chart generation with out-of-control detection | ## Data Model The solution understands standard semiconductor manufacturing data structures: | Entity | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------- | | **Products** | Semiconductor device families, characterized by technology node, foundry, and business unit | | **Lots** | Manufacturing batches with user-friendly IDs (e.g., `L2401-001`) for natural reference in chat and reports | | **Wafers** | Individual wafers within lots with WAT, WSD, and FTD measurements | | **Excursion Events** | Detected yield anomalies with detector type, severity, and affected lots/wafers | | **Reports** | Generated analysis reports (RCA, Insight) with sections, charts, and export support | | **Workflow Runs** | Execution records for Prefect workflows with inputs, status, and outputs | | **Policies** | Learned excursion detection rules with versioning from user feedback | | **Alerts** | Notifications triggered by excursion detection or workflow events | | **Data Source Connections** | Configured connections to external fab data systems |