# Evaluate decision via Agent (legacy) Source: https://docs.gorules.io/api-reference/agent/evaluation/evaluate-decision-via-agent-legacy /openapi/agent.json post /api/projects/{project}/evaluate/{key} Legacy endpoint, still fully supported. New integrations should use `POST /api/rules/{project}/evaluate/{key}`, which shares its surface with the BRMS Rules API. Evaluates a decision model with the provided context and returns the result. Optionally include trace information for debugging. # Get project info Source: https://docs.gorules.io/api-reference/agent/projects/get-project-info /openapi/agent.json get /api/projects/{project} Returns information about the loaded project including the current release version. # Evaluate a rule Source: https://docs.gorules.io/api-reference/agent/rules/evaluate-a-rule /openapi/agent.json post /api/rules/{project}/evaluate/{key} Evaluates a graph or policy from the release loaded by the Agent. Serves the same Rules API surface as BRMS, so clients can switch between BRMS and Agent without changes. Available since Agent 1.28.0; the legacy `/api/projects/{project}/evaluate/{key}` endpoint continues to work. A token is only required when the loaded release ships evaluation tokens: if none are scoped to the deployed target, requests are served without authentication; if at least one is, a matching `X-Access-Token` is required. # OpenAPI document for the loaded release Source: https://docs.gorules.io/api-reference/agent/rules/openapi-document-for-the-loaded-release /openapi/agent.json get /api/rules/{project} Returns an OpenAPI 3 document describing the evaluable rules of the release loaded by the Agent, with input and output schemas inferred from the documents. The same document is served by BRMS for each source (main, branch, commit, release, environment). Available since Agent 1.28.0. A token is only required when the loaded release ships evaluation tokens: if none are scoped to the deployed target, requests are served without authentication; if at least one is, a matching `X-Access-Token` is required. # Agent health check Source: https://docs.gorules.io/api-reference/agent/system/agent-health-check /openapi/agent.json get /api/health Returns the health status of the Agent service. Use this endpoint for load balancer health checks. # Get version Source: https://docs.gorules.io/api-reference/agent/system/get-version /openapi/agent.json get /api/version Returns the current version of the Agent service. # Authentication Source: https://docs.gorules.io/api-reference/authentication Authenticate with GoRules APIs using tokens GoRules APIs support two authentication methods depending on the token type: | Token Type | Header | | --------------------- | ------------------------------- | | Personal Access Token | `Authorization: Bearer ` | | Evaluation Token | `X-Access-Token: ` | ## Personal Access Tokens Personal Access Tokens (PAT) authenticate requests to the BRMS API with your user identity. Use PATs for: * Managing projects, documents, and releases * Administrative operations * CI/CD pipelines that need full API access ### Creating a PAT 1. Navigate to your **Profile** in the BRMS 2. Find the **Personal access token** section 3. Click **Generate token** 4. Configure the token: * **Note**: A description to identify the token's purpose * **Expiry**: Token lifetime (Month, Quarter, Year, or custom) * **All projects**: Toggle to grant access to all projects or select specific ones * **Permissions**: Select the required permissions ### PAT Permissions | Permission | Description | | ------------------- | ------------------------------------------------- | | Project Manage | Members, groups, tokens, approvers, configuration | | Documents | Access to decision documents (Projects v1) | | Branches | Access to branches (Projects v2) | | Integrations manage | Manage integrations (deprecated) | | Releases | Create and manage releases | | Environments | Configure and deploy to environments | Store your PAT securely. It provides access to your BRMS resources based on the permissions granted. ## Evaluation Tokens Evaluation tokens authenticate requests to evaluate decisions. They are scoped to a specific project and used for: * Evaluating decisions via BRMS API * Authenticating with the Agent service * Production workloads ### Creating an Evaluation Token 1. Open your project in the BRMS 2. Go to **Settings** > **Access Tokens** 3. Click **Generate token** 4. Give the token a name ### Using Evaluation Tokens Evaluation tokens work with both the BRMS evaluation endpoints and the Agent service: ```bash BRMS Evaluation theme={null} curl -X POST https://acme.us1.gorules.io/api/rules/{project}/evaluate/{path} \ -H "X-Access-Token: " \ -H "Content-Type: application/json" \ -d '{"context": {"input": "value"}}' ``` ```bash Agent Evaluation theme={null} curl -X POST https://agent.acme.com/api/rules/{project}/evaluate/{path} \ -H "X-Access-Token: " \ -H "Content-Type: application/json" \ -d '{"context": {"input": "value"}}' ``` Evaluation tokens are contained within releases. When you deploy a release to the Agent, the shipped tokens authenticate evaluation requests. If a release ships no tokens for the deployed target, the Agent serves requests without authentication; as soon as at least one token is shipped, a matching `X-Access-Token` is required. ### Legacy evaluation endpoints The previous evaluation endpoints - `/api/projects/{projectId}/evaluate/{path}` and its release and environment variants on BRMS, and `/api/projects/{project}/evaluate/{key}` on the Agent - remain fully supported and authenticate the same way, with an evaluation token in the `X-Access-Token` header. New integrations should use the Rules API endpoints shown above. ## Token Comparison | Feature | Personal Access Token | Evaluation Token | | ------------------------ | --------------------- | -------------------- | | Created in | Profile | Project Settings | | Scope | User-level | Project-level | | BRMS management API | Yes | No | | BRMS evaluation API | Yes | Yes | | Agent API | No | Yes | | Configurable permissions | Yes | No (evaluation only) | # Chat with an AI Source: https://docs.gorules.io/api-reference/brms/ai/chat-with-an-ai /openapi/brms.json post /api/ai/chat Send messages to an AI assistant and receive a streaming response for interactive conversations. # Export audit logs Source: https://docs.gorules.io/api-reference/brms/audit-log/export-audit-logs /openapi/brms.json get /api/audit-logs/export Export audit log entries as a CSV file. Supports filtering by project, user, and date range. # Get audit log Source: https://docs.gorules.io/api-reference/brms/audit-log/get-audit-log /openapi/brms.json get /api/audit-logs/{auditLogId} Retrieve a single audit log entry by ID, including the full data payload. # List audit logs Source: https://docs.gorules.io/api-reference/brms/audit-log/list-audit-logs /openapi/brms.json get /api/audit-logs Retrieve a paginated list of audit log entries for the organisation. Supports filtering by project, user, date range, type, and action. # Branch commit Source: https://docs.gorules.io/api-reference/brms/branches/branch-commit /openapi/brms.json post /api/projects/{projectId}/branches/{branchId}/commit Creates a new commit on a branch with the specified changes. # Create branch Source: https://docs.gorules.io/api-reference/brms/branches/create-branch /openapi/brms.json post /api/projects/{projectId}/branches Creates a new branch in the project. # Get branch Source: https://docs.gorules.io/api-reference/brms/branches/get-branch /openapi/brms.json get /api/projects/{projectId}/branches/{branchId} Retrieves details of a specific branch including its latest commit and change requests. # Get branch commit Source: https://docs.gorules.io/api-reference/brms/branches/get-branch-commit /openapi/brms.json get /api/projects/{projectId}/branches/{branchId}/commits/{commitId} Retrieves a specific commit on a branch by commit ID or keywords. # Get commit Source: https://docs.gorules.io/api-reference/brms/branches/get-commit /openapi/brms.json get /api/projects/{projectId}/commits/{commitId} Retrieves a specific commit by ID. # Get commit diff Source: https://docs.gorules.io/api-reference/brms/branches/get-commit-diff /openapi/brms.json get /api/projects/{projectId}/commits/{commitId}/diff Retrieves the diff (file changes) for a specific commit compared to its previous commit. # Get merge preview Source: https://docs.gorules.io/api-reference/brms/branches/get-merge-preview /openapi/brms.json get /api/projects/{projectId}/branches/{branchId}/merge-preview Previews the result of merging a branch into main, including any conflicts. # List branches Source: https://docs.gorules.io/api-reference/brms/branches/list-branches /openapi/brms.json get /api/projects/{projectId}/branches Retrieves a paginated list of branches for a project. # List commits Source: https://docs.gorules.io/api-reference/brms/branches/list-commits /openapi/brms.json get /api/projects/{projectId}/branches/{branchId}/commits Retrieves a paginated list of commits for a branch. # Merge branch Source: https://docs.gorules.io/api-reference/brms/branches/merge-branch /openapi/brms.json post /api/projects/{projectId}/branches/{branchId}/merge Merges a branch into main, or creates a change request if approval is required. # Rebase branch Source: https://docs.gorules.io/api-reference/brms/branches/rebase-branch /openapi/brms.json post /api/projects/{projectId}/branches/{branchId}/rebase Rebases a branch onto the latest main branch commit. # Remove branch Source: https://docs.gorules.io/api-reference/brms/branches/remove-branch /openapi/brms.json delete /api/projects/{projectId}/branches/{branchId} Deletes a branch from the project. # Revert commit Source: https://docs.gorules.io/api-reference/brms/branches/revert-commit /openapi/brms.json post /api/projects/{projectId}/branches/{branchId}/revert Creates a new branch that reverts the project state to a specific commit. # Create activity Source: https://docs.gorules.io/api-reference/brms/change-requests/create-activity /openapi/brms.json post /api/projects/{projectId}/change-requests/{changeRequestId}/activities Create a new activity on a change request. Activities include approval, rejection, comments, and completion actions. # Get a change request Source: https://docs.gorules.io/api-reference/brms/change-requests/get-a-change-request /openapi/brms.json get /api/projects/{projectId}/change-requests/{changeRequestId} Retrieve a single change request by ID, including activities, snapshot, and related entities. # List change requests Source: https://docs.gorules.io/api-reference/brms/change-requests/list-change-requests /openapi/brms.json get /api/projects/{projectId}/change-requests Retrieve a paginated list of change requests for a project. Supports filtering by document, release, type, and status. # Update activity Source: https://docs.gorules.io/api-reference/brms/change-requests/update-activity /openapi/brms.json put /api/projects/{projectId}/change-requests/{changeRequestId}/activities/{activityId} Update an existing activity on a change request. Allows modifying the comment or resolving the activity. # Evaluate decision Source: https://docs.gorules.io/api-reference/brms/decision/evaluate-decision /openapi/brms.json post /api/projects/{projectId}/evaluate/{*} By default, decision should be published before it is evaluated. # Evaluate environment decision Source: https://docs.gorules.io/api-reference/brms/decision/evaluate-environment-decision /openapi/brms.json post /api/projects/{projectId}/environments/{environmentId}/evaluate/{*} Evaluates a decision using the release deployed to a specific environment. # Evaluate release decisions Source: https://docs.gorules.io/api-reference/brms/decision/evaluate-release-decisions /openapi/brms.json post /api/projects/{projectId}/releases/{releaseId}/evaluate/{*} Evaluates a decision from a specific release version. # HTTP Proxy Source: https://docs.gorules.io/api-reference/brms/decision/http-proxy /openapi/brms.json post /api/projects/{projectId}/http-proxy Proxies an HTTP request through the decision engine for testing integrations. # Simulate decision Source: https://docs.gorules.io/api-reference/brms/decision/simulate-decision /openapi/brms.json post /api/projects/{projectId}/decisions/simulate Simulates a decision graph evaluation without persisting changes, useful for testing decision logic. # Cancel deployment workflow run Source: https://docs.gorules.io/api-reference/brms/deployment-workflows/cancel-deployment-workflow-run /openapi/brms.json post /api/projects/{projectId}/runs/{runId}/cancel Cancel an in-progress or waiting deployment workflow run. # Get deployment workflow run Source: https://docs.gorules.io/api-reference/brms/deployment-workflows/get-deployment-workflow-run /openapi/brms.json get /api/projects/{projectId}/runs/{runId} Retrieve a single deployment workflow run by ID, including all associated jobs. # List deployment workflow runs Source: https://docs.gorules.io/api-reference/brms/deployment-workflows/list-deployment-workflow-runs /openapi/brms.json get /api/projects/{projectId}/runs Retrieve a paginated list of deployment workflow runs for a project. # Create deployment Source: https://docs.gorules.io/api-reference/brms/deployments/create-deployment /openapi/brms.json post /api/deployments Create a new deployment with the specified cloud storage provider configuration. # List deployments Source: https://docs.gorules.io/api-reference/brms/deployments/list-deployments /openapi/brms.json get /api/deployments Retrieve a list of all deployments configured for the current organisation. # Remove deployment Source: https://docs.gorules.io/api-reference/brms/deployments/remove-deployment /openapi/brms.json delete /api/deployments/{deploymentId} Soft delete a deployment by its ID. # Update deployment Source: https://docs.gorules.io/api-reference/brms/deployments/update-deployment /openapi/brms.json put /api/deployments/{deploymentId} Update an existing deployment configuration by its ID. # Create a document Source: https://docs.gorules.io/api-reference/brms/document/create-a-document /openapi/brms.json post /api/projects/{projectId}/documents Create a new document or directory within a project. # Create a version Source: https://docs.gorules.io/api-reference/brms/document/create-a-version /openapi/brms.json post /api/projects/{projectId}/documents/{documentId}/versions Create a new version of a document with updated content. # Delete documents Source: https://docs.gorules.io/api-reference/brms/document/delete-documents /openapi/brms.json delete /api/projects/{projectId}/documents Soft delete one or more documents by moving them to trash. # Download a document Source: https://docs.gorules.io/api-reference/brms/document/download-a-document /openapi/brms.json get /api/projects/{projectId}/documents/{documentId}/download Download the document content as a JSON file. # Get a document Source: https://docs.gorules.io/api-reference/brms/document/get-a-document /openapi/brms.json get /api/projects/{projectId}/documents/{documentId} Retrieve a single document by ID with its latest version and content. # Get a document version Source: https://docs.gorules.io/api-reference/brms/document/get-a-document-version /openapi/brms.json get /api/projects/{projectId}/documents/{documentId}/versions/{versionId} Retrieve a specific version of a document including its content. # Get documents by path Source: https://docs.gorules.io/api-reference/brms/document/get-documents-by-path /openapi/brms.json get /api/projects/{projectId}/documents/path Retrieve a document by its file system path. # List document ancestors Source: https://docs.gorules.io/api-reference/brms/document/list-document-ancestors /openapi/brms.json get /api/projects/{projectId}/documents/{documentId}/ancestors Retrieve the ancestor hierarchy of a document from root to the specified document. # List document paths Source: https://docs.gorules.io/api-reference/brms/document/list-document-paths /openapi/brms.json get /api/projects/{projectId}/documents/list-paths Retrieve a list of all published document paths in a project. # List document versions Source: https://docs.gorules.io/api-reference/brms/document/list-document-versions /openapi/brms.json get /api/projects/{projectId}/documents/{documentId}/versions Retrieve a paginated list of versions for a specific document. # List documents Source: https://docs.gorules.io/api-reference/brms/document/list-documents /openapi/brms.json get /api/projects/{projectId}/documents Retrieve a paginated list of documents for a project with optional filtering. # Move documents Source: https://docs.gorules.io/api-reference/brms/document/move-documents /openapi/brms.json post /api/projects/{projectId}/documents/move Move one or more documents to a new parent directory. # Permanently delete documents Source: https://docs.gorules.io/api-reference/brms/document/permanently-delete-documents /openapi/brms.json delete /api/projects/{projectId}/documents/permanently Permanently remove soft-deleted documents from the system. # Publish a document Source: https://docs.gorules.io/api-reference/brms/document/publish-a-document /openapi/brms.json put /api/projects/{projectId}/documents/{documentId}/publish Publish or unpublish a document version, optionally creating a change request for approval. # Rename a version Source: https://docs.gorules.io/api-reference/brms/document/rename-a-version /openapi/brms.json put /api/projects/{projectId}/documents/{documentId}/versions/{versionId}/rename Update the name of a document version. # Restore document version Source: https://docs.gorules.io/api-reference/brms/document/restore-document-version /openapi/brms.json post /api/projects/{projectId}/documents/{documentId}/versions/restore Restore a document to a previous version by creating a new version with the old content. # Restore documents Source: https://docs.gorules.io/api-reference/brms/document/restore-documents /openapi/brms.json post /api/projects/{projectId}/documents/restore Restore soft-deleted documents from trash to a specified location. # Update a document Source: https://docs.gorules.io/api-reference/brms/document/update-a-document /openapi/brms.json put /api/projects/{projectId}/documents/{documentId} Update document properties such as name or metadata. # Update a document view Source: https://docs.gorules.io/api-reference/brms/document/update-a-document-view /openapi/brms.json put /api/projects/{projectId}/documents/{documentId}/view Update the view configuration and permissions for a document. # Create environment Source: https://docs.gorules.io/api-reference/brms/environment/create-environment /openapi/brms.json post /api/projects/{projectId}/environments Creates a new environment within a project for deploying releases. # Delete environment Source: https://docs.gorules.io/api-reference/brms/environment/delete-environment /openapi/brms.json delete /api/projects/{projectId}/environments/{environmentId} Soft deletes an environment from the project. # List environments Source: https://docs.gorules.io/api-reference/brms/environment/list-environments /openapi/brms.json get /api/projects/{projectId}/environments Retrieves all environments for a project, including their deployment status and pending change requests. # Token regenerate Source: https://docs.gorules.io/api-reference/brms/environment/token-regenerate /openapi/brms.json post /api/projects/{projectId}/environments/{environmentId}/regenerate-token Generates a new access token for the environment, invalidating the previous token. # Undeploy environment Source: https://docs.gorules.io/api-reference/brms/environment/undeploy-environment /openapi/brms.json post /api/projects/{projectId}/environments/{environmentId}/undeploy Removes the currently deployed release from an environment, optionally creating a change request if approval is required. # Update environment Source: https://docs.gorules.io/api-reference/brms/environment/update-environment /openapi/brms.json put /api/projects/{projectId}/environments/{environmentId} Updates an existing environment configuration including name, key, and approval settings. # Create group Source: https://docs.gorules.io/api-reference/brms/group/create-group /openapi/brms.json post /api/projects/{projectId}/groups Create a new group within a project with specified permissions. # Delete group Source: https://docs.gorules.io/api-reference/brms/group/delete-group /openapi/brms.json delete /api/projects/{projectId}/groups/{groupId} Delete a group from a project. Groups managed by access profiles cannot be deleted directly. # List groups Source: https://docs.gorules.io/api-reference/brms/group/list-groups /openapi/brms.json get /api/projects/{projectId}/groups Retrieve a paginated list of groups for a project, with optional search filtering. # Update group Source: https://docs.gorules.io/api-reference/brms/group/update-group /openapi/brms.json put /api/projects/{projectId}/groups/{groupId} Update an existing group with new name, description, or permissions. # Health check Source: https://docs.gorules.io/api-reference/brms/infrastructure/health-check /openapi/brms.json get /api/health Performs a health check on the service, verifying database connectivity. # Create integration Source: https://docs.gorules.io/api-reference/brms/integration/create-integration /openapi/brms.json post /api/projects/{projectId}/integrations Create a new integration within the specified project. # Delete integration Source: https://docs.gorules.io/api-reference/brms/integration/delete-integration /openapi/brms.json delete /api/projects/{projectId}/integrations/{integrationId} Soft delete an integration by its ID. # Get integration Source: https://docs.gorules.io/api-reference/brms/integration/get-integration /openapi/brms.json get /api/projects/{projectId}/integrations/{integrationId} Retrieve a single integration by its ID. # List integrations Source: https://docs.gorules.io/api-reference/brms/integration/list-integrations /openapi/brms.json get /api/projects/{projectId}/integrations Retrieve a paginated list of integrations for the specified project. # Update integration Source: https://docs.gorules.io/api-reference/brms/integration/update-integration /openapi/brms.json put /api/projects/{projectId}/integrations/{integrationId} Update an existing integration by its ID. # List invitations Source: https://docs.gorules.io/api-reference/brms/invitation/list-invitations /openapi/brms.json get /api/invitations Retrieve a paginated list of invitations for the organisation with optional filtering by search term and status. # Delete a member Source: https://docs.gorules.io/api-reference/brms/member/delete-a-member /openapi/brms.json delete /api/projects/{projectId}/members/{memberId} Remove a member from a project. The current user cannot remove themselves using this endpoint. # Invite members Source: https://docs.gorules.io/api-reference/brms/member/invite-members /openapi/brms.json post /api/projects/{projectId}/members Invite one or more users to become members of a project, optionally assigning them to groups and setting owner status. # Join project Source: https://docs.gorules.io/api-reference/brms/member/join-project /openapi/brms.json post /api/projects/{projectId}/members/join Join the project for current user # Leave a project Source: https://docs.gorules.io/api-reference/brms/member/leave-a-project /openapi/brms.json delete /api/projects/{projectId}/members/leave Leaves a project for current user # List member users Source: https://docs.gorules.io/api-reference/brms/member/list-member-users /openapi/brms.json get /api/projects/{projectId}/members/users Retrieve a paginated list of organisation users who are not yet members of the project and can be invited. # List members Source: https://docs.gorules.io/api-reference/brms/member/list-members /openapi/brms.json get /api/projects/{projectId}/members Retrieve a paginated list of all members in a project, including their user details and group assignments. # Update a member Source: https://docs.gorules.io/api-reference/brms/member/update-a-member /openapi/brms.json put /api/projects/{projectId}/members/{memberId} Update an existing project member, including their group assignments and owner status. # Update the organisation Source: https://docs.gorules.io/api-reference/brms/organisation/update-the-organisation /openapi/brms.json put /api/organisations Updates the organisation name, display name, and preferences. # Create service account token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/create-service-account-token /openapi/brms.json post /api/users/service/{userId}/personal-access-tokens Create a new personal access token with specified permissions and project access. # Create token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/create-token /openapi/brms.json post /api/personal-access-tokens Create a new personal access token with specified permissions and project access. # Delete service account token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/delete-service-account-token /openapi/brms.json delete /api/users/service/{userId}/personal-access-tokens/{patId} Note that any pipelines that depend on the token will stop functioning. # Delete token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/delete-token /openapi/brms.json delete /api/personal-access-tokens/{patId} Note that any pipelines that depend on the token will stop functioning. # Get service account token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/get-service-account-token /openapi/brms.json get /api/users/service/{userId}/personal-access-tokens/{patId} Retrieve a single personal access token by ID. # Get token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/get-token /openapi/brms.json get /api/personal-access-tokens/{patId} Retrieve a single personal access token by ID. # List service account tokens Source: https://docs.gorules.io/api-reference/brms/personal-access-token/list-service-account-tokens /openapi/brms.json get /api/users/service/{userId}/personal-access-tokens Retrieve all personal access tokens for the authenticated user or a specified service user. # List tokens Source: https://docs.gorules.io/api-reference/brms/personal-access-token/list-tokens /openapi/brms.json get /api/personal-access-tokens Retrieve all personal access tokens for the authenticated user or a specified service user. # Regenerate service account token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/regenerate-service-account-token /openapi/brms.json post /api/users/service/{userId}/personal-access-tokens/{patId}/regenerate Note that any pipelines that depend on the token will stop functioning. # Regenerate token Source: https://docs.gorules.io/api-reference/brms/personal-access-token/regenerate-token /openapi/brms.json post /api/personal-access-tokens/{patId}/regenerate Note that any pipelines that depend on the token will stop functioning. # Delete user profile Source: https://docs.gorules.io/api-reference/brms/profile/delete-user-profile /openapi/brms.json delete /api/profile Deletes current user profile. This action is not reversible. # Update user profile Source: https://docs.gorules.io/api-reference/brms/profile/update-user-profile /openapi/brms.json put /api/profile Update current user profile. # Visit project Source: https://docs.gorules.io/api-reference/brms/profile/visit-project /openapi/brms.json post /api/profile/visit-project Used for caching last visited project. # Create a project Source: https://docs.gorules.io/api-reference/brms/project/create-a-project /openapi/brms.json post /api/projects Create a new project within the organisation with optional content copying from an existing project. # Delete a project Source: https://docs.gorules.io/api-reference/brms/project/delete-a-project /openapi/brms.json delete /api/projects/{projectId} Soft delete a project, making it recoverable. Protected projects cannot be deleted. # Get a project Source: https://docs.gorules.io/api-reference/brms/project/get-a-project /openapi/brms.json get /api/projects/{projectId} Retrieve detailed information about a specific project by its ID. # Get project with approvals Source: https://docs.gorules.io/api-reference/brms/project/get-project-with-approvals /openapi/brms.json get /api/projects/{projectId}/approvals Retrieve project details including the configured approval groups for decisions. # List projects Source: https://docs.gorules.io/api-reference/brms/project/list-projects /openapi/brms.json get /api/projects Retrieve a paginated list of projects the user has access to within the organisation. # Permanently delete a project Source: https://docs.gorules.io/api-reference/brms/project/permanently-delete-a-project /openapi/brms.json delete /api/projects/{projectId}/permanently Permanently delete a soft-deleted project. This action cannot be undone. # Restore a project Source: https://docs.gorules.io/api-reference/brms/project/restore-a-project /openapi/brms.json put /api/projects/{projectId}/restore Restore a soft-deleted project, making it active again. If the project key conflicts, a new key will be generated. # Update a project Source: https://docs.gorules.io/api-reference/brms/project/update-a-project /openapi/brms.json put /api/projects/{projectId} Update the name, key, or protection status of an existing project. # Update project approvals Source: https://docs.gorules.io/api-reference/brms/project/update-project-approvals /openapi/brms.json put /api/projects/{projectId}/approvals Configure the approval workflow settings for decisions within the project. # Apply a release Source: https://docs.gorules.io/api-reference/brms/release/apply-a-release /openapi/brms.json post /api/projects/{projectId}/releases/apply Apply a release to the project, creating or updating documents from the release files (v1 projects only). # Create a release Source: https://docs.gorules.io/api-reference/brms/release/create-a-release /openapi/brms.json post /api/projects/{projectId}/releases Create a new release from the current project state or a specific commit. # Delete a release Source: https://docs.gorules.io/api-reference/brms/release/delete-a-release /openapi/brms.json delete /api/projects/{projectId}/releases/{releaseId} Soft delete a release from the project. # Deploy a release Source: https://docs.gorules.io/api-reference/brms/release/deploy-a-release /openapi/brms.json post /api/projects/{projectId}/releases/deploy Deploy a release to a specific environment, optionally creating a change request if approvals are required. # Download a release Source: https://docs.gorules.io/api-reference/brms/release/download-a-release /openapi/brms.json get /api/projects/{projectId}/releases/{releaseId}/download Download all files in a release as a ZIP archive. # Get a release Source: https://docs.gorules.io/api-reference/brms/release/get-a-release /openapi/brms.json get /api/projects/{projectId}/releases/{releaseId} Retrieve a single release by ID, optionally including diff information compared to a previous release. # Get a release file Source: https://docs.gorules.io/api-reference/brms/release/get-a-release-file /openapi/brms.json get /api/projects/{projectId}/releases/{releaseId}/files/{fileId} Retrieve a single file from a release including its content. # Get latest published release Source: https://docs.gorules.io/api-reference/brms/release/get-latest-published-release /openapi/brms.json get /api/projects/{projectId}/releases/latest-published Retrieve the most recently published release for a project based on semantic version. # Get release files Source: https://docs.gorules.io/api-reference/brms/release/get-release-files /openapi/brms.json get /api/projects/{projectId}/releases/{releaseId}/files Retrieve all files contained in a release with their content. # List releases Source: https://docs.gorules.io/api-reference/brms/release/list-releases /openapi/brms.json get /api/projects/{projectId}/releases Retrieve a paginated list of releases for a project, with optional filtering by status and search. # Publish release Source: https://docs.gorules.io/api-reference/brms/release/publish-release /openapi/brms.json post /api/projects/{projectId}/releases/{releaseId}/publish Publish a draft release, assigning it a semantic version. # Update release Source: https://docs.gorules.io/api-reference/brms/release/update-release /openapi/brms.json put /api/projects/{projectId}/releases/{releaseId} Update the name, description, or tags of an existing release. # Upload a release Source: https://docs.gorules.io/api-reference/brms/release/upload-a-release /openapi/brms.json post /api/projects/{projectId}/releases/multipart Upload a release package as a ZIP file containing decision documents. # Create role Source: https://docs.gorules.io/api-reference/brms/roles/create-role /openapi/brms.json post /api/roles Creates a new role with the specified permissions and project assignments. # Delete role Source: https://docs.gorules.io/api-reference/brms/roles/delete-role /openapi/brms.json delete /api/roles/{roleId} Deletes a role and removes all associated group assignments. # List roles Source: https://docs.gorules.io/api-reference/brms/roles/list-roles /openapi/brms.json get /api/roles Retrieves a paginated list of roles in the organisation with optional search filtering. # Update role Source: https://docs.gorules.io/api-reference/brms/roles/update-role /openapi/brms.json put /api/roles/{roleId} Updates an existing role with new permissions, users, and project assignments. # Generate a token Source: https://docs.gorules.io/api-reference/brms/security/generate-a-token /openapi/brms.json post /api/projects/{projectId}/security/tokens Generates a new access token for the specified project. # List project tokens Source: https://docs.gorules.io/api-reference/brms/security/list-project-tokens /openapi/brms.json get /api/projects/{projectId}/security/tokens Retrieves all access tokens for the specified project. # Revoke a token Source: https://docs.gorules.io/api-reference/brms/security/revoke-a-token /openapi/brms.json delete /api/projects/{projectId}/security/tokens/{tokenId} Revokes and permanently deletes the specified access token from the project. # Finalize onboarding process with template Source: https://docs.gorules.io/api-reference/brms/templates/finalize-onboarding-process-with-template /openapi/brms.json post /api/onboarding/finalize Completes the onboarding process by creating a new project with the selected template, including a default document and test event. # Get a template Source: https://docs.gorules.io/api-reference/brms/templates/get-a-template /openapi/brms.json get /api/templates/{key} Retrieves a specific template by its unique key, including the full decision graph and sample request JSON. # List templates Source: https://docs.gorules.io/api-reference/brms/templates/list-templates /openapi/brms.json get /api/templates Retrieves a list of all available templates with their summary information. # Skip onboarding process Source: https://docs.gorules.io/api-reference/brms/templates/skip-onboarding-process /openapi/brms.json post /api/onboarding/skip Marks the onboarding process as complete without creating a project from a template. # Create test event Source: https://docs.gorules.io/api-reference/brms/test-events/create-test-event /openapi/brms.json post /api/projects/{projectId}/test-events Create a new test event within a project for testing decision logic with sample data. # List test events Source: https://docs.gorules.io/api-reference/brms/test-events/list-test-events /openapi/brms.json get /api/projects/{projectId}/test-events Retrieve all test events for a project, optionally filtered by document ID. # Remove test event Source: https://docs.gorules.io/api-reference/brms/test-events/remove-test-event /openapi/brms.json delete /api/projects/{projectId}/test-events/{testEventId} Delete an existing test event from a project. # Update test event Source: https://docs.gorules.io/api-reference/brms/test-events/update-test-event /openapi/brms.json put /api/projects/{projectId}/test-events/{testEventId} Update an existing test event with partial data. # Upsert test event Source: https://docs.gorules.io/api-reference/brms/test-events/upsert-test-event /openapi/brms.json patch /api/projects/{projectId}/test-events Create a new test event or update an existing one if a matching ID is provided. # Create service account Source: https://docs.gorules.io/api-reference/brms/user/create-service-account /openapi/brms.json post /api/users/service Creates a new service account user with the specified email and user type. # Invite users Source: https://docs.gorules.io/api-reference/brms/user/invite-users /openapi/brms.json post /api/users Sends invitation emails to the specified email addresses to join the organisation. # List users Source: https://docs.gorules.io/api-reference/brms/user/list-users /openapi/brms.json get /api/users Retrieves a paginated list of users in the organisation with optional filtering by service account type and search term. # Remove a user Source: https://docs.gorules.io/api-reference/brms/user/remove-a-user /openapi/brms.json delete /api/users/{userId} Removes a user from the organisation, deleting their memberships, invitations, and sessions. # Revoke a user invitation Source: https://docs.gorules.io/api-reference/brms/user/revoke-a-user-invitation /openapi/brms.json delete /api/users/{userId}/invitation Revokes a pending invitation for a user, preventing them from joining the organisation. # Send a user invitation Source: https://docs.gorules.io/api-reference/brms/user/send-a-user-invitation /openapi/brms.json post /api/users/{userId}/invitation Sends or resends an invitation email to a user who has been invited but not yet accepted. # Transfer ownership Source: https://docs.gorules.io/api-reference/brms/user/transfer-ownership /openapi/brms.json post /api/users/{userId}/transfer-ownership Transfers the ownership of organisation to given user. # Update a user Source: https://docs.gorules.io/api-reference/brms/user/update-a-user /openapi/brms.json put /api/users/{userId} Updates user properties including name, status, type, and assigned roles. # Evaluate decision Source: https://docs.gorules.io/api-reference/decision/evaluate-decision /openapi/brms.json post /api/projects/{projectId}/evaluate/{*} By default, decision should be published before it is evaluated. # Evaluate environment decision Source: https://docs.gorules.io/api-reference/decision/evaluate-environment-decision /openapi/brms.json post /api/projects/{projectId}/environments/{environmentId}/evaluate/{*} Evaluates a decision using the release deployed to a specific environment. # Evaluate release decisions Source: https://docs.gorules.io/api-reference/decision/evaluate-release-decisions /openapi/brms.json post /api/projects/{projectId}/releases/{releaseId}/evaluate/{*} Evaluates a decision from a specific release version. # Evaluate decision via Agent (legacy) Source: https://docs.gorules.io/api-reference/evaluation/evaluate-decision-via-agent-legacy /openapi/agent.json post /api/projects/{project}/evaluate/{key} Legacy endpoint, still fully supported. New integrations should use `POST /api/rules/{project}/evaluate/{key}`, which shares its surface with the BRMS Rules API. Evaluates a decision model with the provided context and returns the result. Optionally include trace information for debugging. # Introduction Source: https://docs.gorules.io/api-reference/introduction Overview of GoRules APIs GoRules provides two services with REST APIs. Download the BRMS API specification Download the Agent API specification ## BRMS API The Business Rules Management System (BRMS) API provides full access to manage your rules, projects, releases, environments, and users. Use this API to: * Manage projects and documents * Create and deploy releases * Configure environments * Evaluate decisions directly through BRMS * Administer users and permissions Rule evaluation is served by the **Rules API** (`/api/rules/{projectId}/...`), which evaluates against the main branch, a branch, a commit, a release, or an environment, and serves a per-source OpenAPI document with input and output schemas inferred from your rules - see [Developer tools](/developers/developer-tools). The legacy `/api/projects/{projectId}/evaluate` endpoints continue to work. The BRMS API requires authentication via [Personal Access Token (PAT)](/api-reference/authentication#personal-access-tokens) or [Evaluation Token](/api-reference/authentication#evaluation-tokens) depending on the endpoint. The BRMS API is under active development. Some endpoints may change in future releases and response schemas may be incomplete or inaccurate. If you have integrations using the BRMS API, test them when upgrading to a new version. ## Agent API The Agent is a lightweight, high-performance service designed for rule evaluation at scale. It loads decision models from releases and executes them with minimal latency. Use the Agent API to: * Evaluate decisions in production environments * Handle high-throughput workloads * Deploy rules closer to your application Since Agent 1.28.0, the Agent serves the same Rules API surface as BRMS: `POST /api/rules/{project}/evaluate/{path}` for evaluation and `GET /api/rules/{project}` for the OpenAPI document of the loaded release. Clients built against the BRMS Rules API work against the Agent without changes, and the legacy `/api/projects/{project}/evaluate/{key}` endpoint continues to work. The Agent authenticates with [Evaluation Tokens](/api-reference/authentication#evaluation-tokens) shipped inside the deployed release. If no tokens are scoped to the deployed target, the Agent serves requests without authentication; as soon as at least one token is shipped, a matching `X-Access-Token` header is required. For deployment options and configuration, see the [Agent deployment guide](/developers/deployment/agent). ## Choosing between BRMS and Agent | Use Case | Service | | --------------------------- | ------- | | Managing rules and projects | BRMS | | CI/CD integration | BRMS | | Development and testing | BRMS | | Production rule evaluation | Agent | | High-throughput evaluation | Agent | | Low-latency requirements | Agent | For production workloads, we recommend using the Agent for evaluation while using the BRMS API for management operations. # Evaluate a rule Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule /openapi/agent.json post /api/rules/{project}/evaluate/{key} Evaluates a graph or policy from the release loaded by the Agent. Serves the same Rules API surface as BRMS, so clients can switch between BRMS and Agent without changes. Available since Agent 1.28.0; the legacy `/api/projects/{project}/evaluate/{key}` endpoint continues to work. A token is only required when the loaded release ships evaluation tokens: if none are scoped to the deployed target, requests are served without authentication; if at least one is, a matching `X-Access-Token` is required. # Evaluate a rule against a pinned release Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule-against-a-pinned-release /openapi/brms.json post /api/rules/{projectId}/release/{releaseId}/evaluate/{*} Evaluates a rule against the decision content of a pinned release. # Evaluate a rule against a specific commit Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule-against-a-specific-commit /openapi/brms.json post /api/rules/{projectId}/commit/{commitId}/evaluate/{*} Evaluates a rule against the project state at a specific commit. # Evaluate a rule in an environment Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule-in-an-environment /openapi/brms.json post /api/rules/{projectId}/env/{environmentId}/evaluate/{*} Evaluates a rule against the release an environment points to. # Evaluate a rule on a branch Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule-on-a-branch /openapi/brms.json post /api/rules/{projectId}/branch/{branchId}/evaluate/{*} Evaluates a rule against the latest commit on a branch. # Evaluate a rule on the main branch Source: https://docs.gorules.io/api-reference/rules/evaluate-a-rule-on-the-main-branch /openapi/brms.json post /api/rules/{projectId}/evaluate/{*} Evaluates a rule against the latest main-branch commit. # OpenAPI document for the loaded release Source: https://docs.gorules.io/api-reference/rules/openapi-document-for-the-loaded-release /openapi/agent.json get /api/rules/{project} Returns an OpenAPI 3 document describing the evaluable rules of the release loaded by the Agent, with input and output schemas inferred from the documents. The same document is served by BRMS for each source (main, branch, commit, release, environment). Available since Agent 1.28.0. A token is only required when the loaded release ships evaluation tokens: if none are scoped to the deployed target, requests are served without authentication; if at least one is, a matching `X-Access-Token` is required. # Rules OpenAPI document for a branch Source: https://docs.gorules.io/api-reference/rules/rules-openapi-document-for-a-branch /openapi/brms.json get /api/rules/{projectId}/branch/{branchId} Returns an OpenAPI document describing the evaluable rules on the latest commit of a branch. # Rules OpenAPI document for a commit Source: https://docs.gorules.io/api-reference/rules/rules-openapi-document-for-a-commit /openapi/brms.json get /api/rules/{projectId}/commit/{commitId} Returns an OpenAPI document describing the evaluable rules at a specific commit. # Rules OpenAPI document for a release Source: https://docs.gorules.io/api-reference/rules/rules-openapi-document-for-a-release /openapi/brms.json get /api/rules/{projectId}/release/{releaseId} Returns an OpenAPI document describing the evaluable rules inside a release. # Rules OpenAPI document for an environment Source: https://docs.gorules.io/api-reference/rules/rules-openapi-document-for-an-environment /openapi/brms.json get /api/rules/{projectId}/env/{environmentId} Returns an OpenAPI document describing the evaluable rules inside the release an environment points to. # Rules OpenAPI document for the main branch Source: https://docs.gorules.io/api-reference/rules/rules-openapi-document-for-the-main-branch /openapi/brms.json get /api/rules/{projectId} Returns an OpenAPI document describing the evaluable rules on the latest main-branch commit. # Audit Logs Source: https://docs.gorules.io/brms/administration/audit-logs Track all changes and actions in your organisation. Audit logs record every significant action in your organisation. Use them for compliance, debugging, and understanding who changed what. ## Viewing logs 1. Go to Organisation settings → Audit logs 2. Browse the chronological list of actions 3. Use filters to narrow results ## Log entry fields | Field | Description | | ---------- | ---------------------------------------------- | | User | Who performed the action | | Action | What was done (created, invited, signup, etc.) | | Type | Resource type affected (project, member, user) | | IP Address | Origin IP of the request | | Time | When the action occurred | ## Filtering options ### By project Filter to see actions affecting a specific project only. ### By user View all actions performed by a specific team member. ### By date range Narrow results to a specific time period: 1. Click the From Date field 2. Select start date 3. Click To Date field 4. Select end date ## Exporting logs Download logs for external analysis or compliance records: 1. Apply desired filters 2. Click "Export" 3. Download the generated file ## Common logged actions | Action | Type | Description | | -------- | -------- | ------------------------------- | | signup | user | New user registration | | invited | member | User invited to project | | created | project | New project created | | updated | settings | Configuration changed | | deployed | release | Release deployed to environment | | merged | branch | Branch merged to main | # Members & Groups Source: https://docs.gorules.io/brms/administration/members-groups Manage project team access and create permission groups. Members & Groups controls who can access your project and what actions they can perform. Access from Settings → Members & Groups within a project. ## Members The Members tab shows everyone with access to the project. ### Member information | Column | Description | | ------- | ---------------------------------------------------- | | User | Member's email with role badge (Admin if applicable) | | Groups | Permission groups the member belongs to | | Actions | Edit membership or remove from project | ### Inviting members Add organization users to your project: 1. Click "Invite member" 2. Select users from the dropdown 3. Optionally assign to one or more groups 4. Check "Admin" to grant full project access 5. Click Invite If a user isn't listed, they need to be added to the organization first. Click the "Users" link in the modal to navigate to organization user management. ### Project admins Members with the Admin flag have full access to the project, including: * All permissions regardless of group membership * Ability to manage other members and groups * Access to all project settings ### Removing members Click the remove action next to any member to revoke their project access. This doesn't affect their organization membership. ## Groups Groups bundle permissions together for easier management. Assign members to groups instead of managing individual permissions. ### Creating a group 1. Go to the Groups tab 2. Click "Create" 3. Enter name and description 4. Select permissions from the tree 5. Configure file permissions (optional) 6. Click Create ### Group properties | Property | Description | | ---------------- | ----------------------- | | Name | Group identifier | | Description | Purpose of the group | | Permissions | Selected capabilities | | File Permissions | Path-based access rules | ### Permission categories Select permissions from these categories: | Category | Available permissions | | -------------- | ------------------------------------------------- | | Project Manage | Members, groups, tokens, approvers, configuration | | Documents (v1) | Full, Edit Content, View Content, Edit View | | Branches (v2) | Create, Merge, Delete | | Releases | Manage, Deploy, Delete | | Environments | Manage, Delete | ### File permissions Add granular access rules for specific files or folders. **Configuration:** | Field | Options | | -------- | ---------------------------------------------- | | Effect | Allow or Deny | | Patterns | Glob patterns matching files | | Actions | Visible, Modify, Write (create/update), Delete | **Pattern examples:** | Pattern | Matches | | ----------- | ---------------------------------- | | `**` | All files in the project | | `folder/*` | Files directly in folder | | `folder/**` | All files in folder and subfolders | | `pricing/*` | All files in the pricing folder | | `*.json` | All JSON files | **Rule evaluation:** When multiple rules match a file, Deny rules take precedence over Allow rules. Use this to grant broad access with specific restrictions. ### Editing groups Click the edit action on any group to modify its permissions or file access rules. ### Deleting groups Remove a group to revoke its permissions from all members. Members remain in the project but lose the group's permissions unless granted through another group. # Project Settings Source: https://docs.gorules.io/brms/administration/project-settings Configure general project settings including branch protection and project details. Project settings control core project behavior. Access from the gear icon within any project, then select General. ## Main branch protection rules Protect the main branch by requiring approval before changes can be merged. ### Approval modes | Mode | Behavior | | --------------- | ----------------------------------------------------------------- | | None | Direct commits allowed, no review process | | Open Request | Creates a change request for visibility, but no approval required | | Single Approval | One team member must approve before merge | | Group Approval | One member from each assigned group must approve | ### Configuring protection 1. Go to Settings → General 2. Expand "Main branch protection rules" 3. Select an approval mode 4. Click Update ## Update project Change the project name or identifier. | Field | Description | | ----------- | ----------------------------------------------------- | | Name | Display name shown in the UI | | Project key | URL-friendly identifier used in APIs and integrations | ### Changing the project key The project key appears in URLs and API calls. Changing it affects all existing integrations. 1. Expand "Update project" 2. Enter the new name or project key 3. Check "I accept the risk" (required for key changes) 4. Click Update Update any external integrations to use the new project key after changing it. ## Danger zone Permanently delete the project and all its data. ### Deleting a project 1. Expand "Danger zone" 2. Check "I would like to delete this project" 3. Click "Delete project" 4. Confirm the deletion This action is irreversible. All rules, releases, branches, and history will be permanently removed. # GoRules AI Source: https://docs.gorules.io/brms/build/ai Build, test, and debug graphs and policies with the built-in AI assistant. GoRules AI is the assistant built into every project. Describe what you need - a new rule, a failing test explained, a rename across the whole workspace - and it reads your documents, makes the changes, and shows you exactly what it did. GoRules AI page with a chat input and the workspace sidebar ## Where you'll find it * **GoRules AI** in the sidebar's Quick Access opens a project-wide chat that can work across every document in the workspace. * The **GoRules AI** tab in a document's side panel opens the same assistant scoped to the graph or policy you're editing. Both share one engine; the difference is starting context. ## What it can do * **Build and edit** - create graphs and policies, add and rewire nodes, edit decision table rows, write expressions. All node and block types are supported. * **Test** - write test cases, run them, and debug failures by simulating with trace data. * **Simulate and explain** - evaluate a graph with sample input, inspect intermediate values, and explain how a result was derived. * **Analyse and refactor** - search the whole workspace, follow dependencies between documents, and rename an entity or field everywhere it's used. * **Check quality** - run the same checks as [Quality Control](/brms/quality/quality-control) and fix what they find. After it acts, the assistant reports an action log - files changed, tests run, simulations - so you can verify each step. Changes land in your draft workspace like your own edits: review them in the [Changes panel](/brms/build/workspace) and revert anything you don't want. For complex or ambiguous requests the assistant presents a plan, or asks you to choose between options, before touching anything. ### Attachments Attach images and files with the paperclip, by drag-and-drop, or by pasting. Attach a CSV of reference data and ask the assistant to turn it into a dictionary or a decision table. ## Scope awareness The assistant adapts to what you have open. On a branch draft it has full editing capability. On a release, a commit, or a request diff it switches to read-and-simulate: it can explain and evaluate, but not modify history. ## Prompts that work well Be specific. The more concrete the request, the better the result. | Goal | Good prompt | | -------- | ---------------------------------------------------------------------------------------------------------------------- | | Build | "Create a decision table for shipping rates based on weight (\< 1kg, 1-5kg, > 5kg) and zone (domestic, international)" | | Modify | "In the pricing table, change row 2 to a 20% discount instead of 15%" | | Test | "Add test cases for the loan-approval graph covering a strong applicant and one below the credit minimum" | | Debug | "This test fails - simulate its input and show me where the trace diverges from the expected output" | | Refactor | "Rename the `tier` field to `customerTier` everywhere it's used" | Vague prompts - "make it better", "fix the bug" - work less well than describing the expected behaviour. ## Long conversations A meter shows how much of the context window the chat has used. When a conversation gets long, use **Compact history** to summarise it in place, or **Clear chat** to start fresh. Compacting keeps the assistant sharp without losing the thread. ## Availability and credits GoRules AI requires a plan with AI enabled and an LLM provider configured by your administrator - see [AI setup](/developers/deployment/brms/ai-setup). Usage draws on your organisation's AI credits: a monthly included allowance is used first (it resets monthly and doesn't roll over), then any purchased top-ups. When the balance reaches zero, AI pauses until you top up. Administrators can track spend under Billing & Credits in the organisation settings. ## Connecting external AI tools The same tools the assistant uses are available to external AI editors - Claude Code, Cursor, and others - through the MCP server in [Developer Tools](/developers/developer-tools#mcp-server). Connect the [GoRules CLI](/developers/cli) and your editor works with the workspace exactly as GoRules AI does. # Decision graphs Source: https://docs.gorules.io/brms/build/graphs Build and edit decision graphs on the visual canvas, with typed interfaces, imports, and live problem detection. A decision graph connects nodes - inputs, decision tables, expressions, switches, functions - into a flow that turns an input object into a decision. The graph editor gives you a visual canvas with a side panel for the graph's interface, tests, problems, and GoRules AI. Graph editor showing a loan approval flow with input, expression, table, and output nodes, a lending-policy import chip, and the Interface panel ## The canvas Add nodes from the canvas, connect them with edges, and drag to rearrange. Each node has quick actions for duplicating, copying, and deleting, and a settings dialog for its configuration - hit policy for decision tables, execution mode, input field, output path, and pass-through behaviour. Zoom controls sit in the bottom-right corner. **Fit view** centres the whole graph. ## Importing policies Click **Import** above the canvas to import a [policy](/brms/build/policies). Imported policies appear as chips; each chip opens the policy or removes the import. Importing a policy brings its dictionaries into the graph. Dictionary values become available as decision table column types and in schema definitions, so business users pick from labelled options instead of typing raw values. In the input schema, reference a dictionary as a field type with `{"$dictionary": "employmentStatus"}`. At evaluation time the engine expands it into the dictionary's value set and rejects inputs outside it - the vocabulary you defined once in a policy also guards what callers can send. ## The side panel ### Interface The **Interface** tab shows the graph's contract: every input property with its type, and every output the graph computes. The input schema is editable here - and it matters. A graph without a real input schema cannot be statically checked, so define your inputs early. Interface panel showing typed applicant and loan input trees and computed outputs including status and reason ### Problems The **Problems** tab lists everything [static analysis](/brms/quality/static-analysis) found in this graph - type mismatches, unreachable nodes, redundant logic - each with an explanation and a suggested fix. Click a problem to jump to the node it came from. ### Tests The **Tests** tab runs the graph's [test cases](/brms/build/testing) without leaving the editor. ### GoRules AI The **GoRules AI** tab opens the [assistant](/brms/build/ai) scoped to this graph. Ask it to explain the flow, add nodes, or write tests. ## Simulating Open the simulate panel to run the graph with a JSON input. You get the output, per-node trace data, and an **Explain** action on values so you can see how a result was derived. A good simulation run can be promoted to a saved test event, so a one-off debugging session becomes a permanent regression check. ## Decision tables Decision tables evaluate rows top to bottom - conditions on the left, outputs on the right. Two capabilities worth knowing: * Output columns can be typed, including with dictionary types from imported policies. Typed columns validate their cells and offer labelled dropdowns in business mode. * Tables support Excel and CSV import and export, with a column-mapping step when the file doesn't match the table exactly. ## Related pages Shared definitions your graphs can import. What the Problems tab checks for. # Policies Source: https://docs.gorules.io/brms/build/policies Define shared dictionaries, data models, and rules once - then import them into any graph. A policy is a document for the definitions and rules your whole project should agree on: the valid values for a field, the shape of your core entities, and the derived facts other rules build on. Write them once in a policy, then import the policy wherever it's needed. Unlike a graph, a policy has no nodes or edges. It reads like a document - text and rule blocks mixed together - and the engine works out the evaluation order automatically from what each block reads and writes. Policy editor showing an employmentStatus dictionary, an applicant data model, and a computed rule, with the Entities panel open If **Policy** doesn't appear in the New dialog, policies are not enabled for your organisation. Ask your administrator or contact support about enabling them. ## Writing a policy Type `/` anywhere in the document to insert a block. Text blocks (headings, paragraphs, lists) document your rules; rule blocks define them: | Block | What it does | | -------------- | ------------------------------------------------- | | Dictionary | A named list of valid values with display labels. | | Data Model | Declares an entity and its typed properties. | | Expression | Computes a value and writes it to a property. | | Decision Table | Spreadsheet-style rules, same as in a graph. | | Match | Routes between outcomes based on conditions. | | Assertion | States a condition that must hold. | | Globals | Shared values available everywhere. | Block order doesn't matter. If one block computes `applicant.debtToIncome` and another reads it, the engine runs them in the right order - even across imported policies. ## Dictionaries A dictionary is a named set of values with human-readable labels: | Value | Label | | --------------- | ------------- | | `EMPLOYED` | Employed | | `SELF_EMPLOYED` | Self-employed | | `RETIRED` | Retired | | `UNEMPLOYED` | Unemployed | The value is what rules compare against and what evaluation sees; the label is what business users read. In business mode, rules render with labels - "applicant.employment *is one of* Employed, Self-employed". Use a dictionary's name as a type: a data model property typed `employmentStatus` only accepts those four values, and [static analysis](/brms/quality/static-analysis) flags anything else at edit time. Graphs that import the policy can do the same in their input schema - a field defined as `{"$dictionary": "employmentStatus"}` accepts only those values at evaluation time. Dictionaries also support CSV import and export for longer value lists. ## Data models A data model declares an entity - `applicant`, `customer`, `order` - and its properties with types: `string`, `number`, `boolean`, `date`, or any dictionary. Properties can be arrays or optional. Once declared, every rule that touches the entity is type-checked against it. ## Importing Graphs import policies with the **Import** button on the canvas; policies import other policies through the `imports` in their header. An import brings the whole chain: importing a policy also pulls in everything it imports. Imported policies evaluate together as one unit in a shared namespace. That has a practical consequence: two blocks writing the same property is a conflict (`DUPLICATE_WRITER`) even when they live in different files - the analyzer checks the whole import group together. ## Evaluating policies A policy isn't just a library - it's directly executable. Send it an input object and it returns the object enriched with everything its blocks computed. Every policy gets its own HTTP endpoint alongside your graphs; see [Developer Tools](/developers/developer-tools). ## The side panel The **Entities** tab lists every entity the policy defines or touches: each property's type, whether it's declared or computed, which blocks write it, and where it's used. The **Problems** tab shows [static analysis](/brms/quality/static-analysis) findings for the policy. ## Related pages Import policies into your graphs. How policies are checked across imports. # Testing Source: https://docs.gorules.io/brms/build/testing Add test files next to your graphs and keep every rule change covered. Tests in GoRules are documents, just like graphs. A test file holds named cases - an input and the output you expect - and runs them against a target graph. Because tests live on the branch with the rules they cover, changing a rule and updating its tests is one review, one commit. ## Creating a test file Click **New** in the workspace and choose **Test**. Name the file after the graph it covers with a `.test` suffix - `loan-approval.test` for the `loan-approval` graph - and it's picked up automatically by [Quality Control](/brms/quality/quality-control) and the graph's Tests panel. Each case in the test editor has three panes: | Pane | What it holds | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Input | The JSON object sent to the graph. | | Expected output | The fields you assert on. Matching is by subset - the case passes if the actual output contains every expected field with the expected value. | | Actual output | What the last run produced, side by side with what you expected. | Give cases descriptive names - "declined - low credit score", "review - high debt-to-income" - so a failure tells you what broke without opening the case. ## Running tests from the graph The **Tests** tab in the graph editor shows every case that targets the open graph, with a live status hero: "All cases passing", or the number of failing cases. Tests panel showing All cases passing with five named test cases and an Events section From here you can: * **Run all** cases, run a single file, or run one case. * **Add case** to append a new case to the file. * Disable a case to skip it temporarily, or make it private so it only runs for you. * **Simulate** the graph ad hoc, then promote a good run to a saved **event** - a captured real input that becomes a permanent regression check. ## Events Events are captured evaluations: instead of hand-writing input JSON, you save an actual simulation (or a real traced request) as a test. They appear in the **Events** section of the Tests panel and run with the rest of the suite. ## Tests as a quality gate Test results feed two places beyond the editor: * [Quality Control](/brms/quality/quality-control) aggregates every test file in the workspace, tracks which graphs have no coverage at all, and exports a PDF report. * [Pre-flight checks](/brms/review/review) on the Review page run your tests before you save, so failing cases are visible at the moment you commit. Graphs without a test file are listed as "untested" on the Quality Control page. Adding even one happy-path case gets a graph onto the coverage radar. # Workspace Source: https://docs.gorules.io/brms/build/workspace Browse, create, and organise the documents in your project - graphs, policies, tests, and folders. The workspace is your project's home. The sidebar lists every document on the current branch, tracks your unsaved changes, and gives you one-click access to GoRules AI, Quality Control, and Developer Tools. Workspace sidebar showing Quick Access, Changes, and Workspace sections alongside the GoRules AI page ## The sidebar | Section | What it contains | | --------------------------- | ----------------------------------------------------------------------------------------------- | | Branch selector | The branch you are working on. Every edit is a draft on this branch until you save it. | | Search | Press ⌘K to search projects, files, and branches - and the content inside documents. | | History, Requests, Releases | Commit history, open [requests](/brms/review/requests), and [releases](/brms/deploy/releases). | | Quick Access | GoRules AI, [Quality Control](/brms/quality/quality-control), and Developer Tools. | | Changes | Appears once you have unsaved edits. Revert individual files, folders, or everything. | | Workspace | The file tree. Hover a file to pin it; pinned files stay at hand across sessions. | | Review | Opens [Review & save](/brms/review/review) with a badge showing how many changes are ready. | ## Document types Click **New** in the Workspace section to create a document: | Type | What it is | | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | Graph | A [decision graph](/brms/build/graphs) - nodes and edges that evaluate an input to an output. | | Policy | A [policy](/brms/build/policies) - shared definitions, dictionaries, and rules that graphs can import. | | Test | A [test file](/brms/build/testing) - named test cases that run against a graph. Name it after the graph with a `.test` suffix. | | Folder | A directory for organising documents. | New document dialog with Graph, Policy, Test, and Folder tabs ## Working with files Right-click a file for its actions: Open, Rename, Move to, Copy to, Download, Copy URL, and Reset to discard local changes. Folders additionally offer Import, Copy all, and Download all as zip. You can also drag files - or a whole zip archive - from your desktop into the workspace to import them. Everything you edit stays a local draft on your branch until you save it from the [Review page](/brms/review/review). The **Changes** section shows exactly what you've touched, so you can revert a single file or all changes before committing. ## Searching inside documents The ⌘K search looks inside your documents, not just at their names. It matches headings, expressions, decision table cells, dictionary entries, data model properties, node names, and more, then deep-links you to the exact block or node. Use it to answer questions like "where do we reference `customer.tier`?" without opening files one by one. ## Business and developer modes The **Developer mode** switch in the editor header changes how rules are displayed: * **Business mode** (default off state) renders expressions as natural-language sentences - "applicant.employment *is one of* Employed, Self-employed" - using the labels from your [dictionaries](/brms/build/policies#dictionaries). * **Developer mode** shows the underlying ZEN expressions. The mode is a personal display preference. It never changes what is stored or evaluated. See [Natural language](/learn/authoring/natural-language) for how sentences map to expressions. ## Next steps Build rules on the visual canvas. Share definitions and dictionaries across graphs. Add test files and keep coverage green. Commit your changes with pre-flight checks. # Environments Source: https://docs.gorules.io/brms/deploy/environments Set up deploy targets like staging and production, with approval rules for each. An environment is a deploy target that serves exactly one release at a time - for example staging and production. Your applications evaluate decisions against an environment, so the rules they run change only when you deploy a new release to it. That separation lets you keep editing and testing freely while production stays on a version you trust. Environments are configured in project settings and appear at the top of the [Releases](/brms/deploy/releases) page. If none exist yet, the Releases page shows "No environments configured yet. Add one from project settings to start deploying." ## Stage and deployment environments GoRules supports two environment types: | Type | How it works | Best for | | ---------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | Stage | The release is served by GoRules and evaluated over its HTTP endpoint | Most teams; no infrastructure to run | | Deployment | The release is pushed to your object storage (S3, Azure, GCS) and pulled by the GoRules Agent or an embedded SDK | High volume, edge, or offline evaluation | ### Stage environments Stage environments are hosted in GoRules. Deploying is instant, and your applications call the environment's evaluate endpoint over HTTP - see [Developer tools](/developers/developer-tools) for endpoints and code samples. ```mermaid theme={null} flowchart LR app["Your App"] --> brms["GoRules"] subgraph brms["GoRules"] staging["staging: v1.5.0"] production["production: v1.4.3"] end ``` ### Deployment environments Deployment environments push each release as a bundle to a deployment target - object storage that agents or embedded SDKs pull from and evaluate locally. Evaluation keeps working from the last pushed bundle even if GoRules is unreachable. ```mermaid theme={null} flowchart LR brms["GoRules"] -->|push| storage["Object Storage"] storage -->|pull| agent["Agent / SDK"] app["Your App"] --> agent ``` Deployment targets are configured separately - see [Deployments](/brms/setup/deployments). The two types behave differently when a release is removed: stage environments serving it stop evaluating, while deployment environments keep their last pushed bundle and continue working until a new release is deployed. ## Environment status Each environment card on the Releases page shows the release it serves and its current state: | Status | Meaning | | ---------- | ------------------------------------------------------------------------ | | live | The release deployed successfully and is being served | | deploying | A deployment is in progress | | failed | The last deployment did not complete; the previous release keeps serving | | no release | Nothing has been deployed to this environment yet | Cards also show how long ago the last deployment happened. When you're deploying, an environment already serving the selected release is marked "already live". ## Create an environment 1. Open project settings and go to **Environments**. 2. Click **New environment**. 3. Enter a name, such as "staging" or "production", and a short key, such as "stg" or "prod". 4. Pick the type: Stage or Deployment. For a deployment environment, select an existing deployment target or create a new one by choosing a provider. 5. Choose a deploy approval mode, and for Group Approval, select the approval groups. ## Deploy approval modes Approval modes control what happens when someone deploys a release to the environment: | Mode | Behaviour | | --------------- | ------------------------------------------------------------ | | None | Deployments go through immediately | | Open Request | Deploying opens a request for visibility; no approval needed | | Single Approval | One approver must approve before the release goes live | | Group Approval | One member from each assigned group must approve | Environments with an approval mode other than None show a "requires review" chip in the deploy view. Deploying to them opens a deploy request instead of deploying directly, and the release goes live once the request is approved. See [Requests](/brms/review/requests). Use None for development environments where speed matters, and an approval mode for production so every change gets a second pair of eyes. ## Evaluating against an environment Every stage environment has its own evaluate endpoint. Point your application at the environment - not at a specific release - and deployments swap the rules behind the endpoint without any change on your side. Authenticate with a project access token; you can scope a token to a single environment so production credentials can't evaluate anything else. See [Developer tools](/developers/developer-tools) for endpoints, tokens, and code samples. Older projects may still have a per-environment access token. These are deprecated - create a project access token scoped to the environment instead. ## Undeploy or delete an environment To stop serving a release without deleting the environment, use **Undeploy** in the environment's row menu in settings. Consumers of the environment immediately stop receiving the current release. Deleting an environment removes it as a deploy target and undeploys any release it was serving. Both actions ask you to type the environment's name to confirm. # Releases Source: https://docs.gorules.io/brms/deploy/releases Package a branch into a versioned snapshot and push it to your environments. A release is a snapshot of a branch at a point in time. Once created, its contents never change - you deploy it to an environment, download it, or return to it later if you need to roll back. Releases store content by reference, so creating one is fast and doesn't duplicate your files, no matter how large your project is. Releases page showing environment cards and a filterable list of published and draft releases ## The Releases page Open **Releases** from the sidebar. The page has two sections: * Your environments at the top - the deploy targets for this project and which release each one is serving. If you see "No environments configured yet. Add one from project settings to start deploying.", set one up first. See [Environments](/brms/deploy/environments). * All releases below, filtered by **Published**, **Drafts**, or **All**. Each release shows its version or name, a draft or published badge, who created it and when, its description, and a "live on" chip for every environment currently serving it. The row menu offers **Publish…**, **Update…**, and **Remove…**, and a download button saves the release as a ZIP. ## Create a release 1. Click **Create release**. 2. Pick the branch to snapshot. 3. Enter a name and, optionally, a description of what's in the release. 4. Choose how the version should change: **Patch**, **Minor**, or **Major**. You can also keep the release as a draft and publish it later. 5. Review the Pre-flight block, then click **Create release**. As soon as you pick a branch, pre-flight checks run against it: test cases, static analysis, and whether environments are available to deploy to. Green checks mean the branch is in good shape to release. Pre-flight is the same safety net you see when saving changes - see [Review & save](/brms/review/review). ### Choosing a version increment Published releases follow semantic versioning (MAJOR.MINOR.PATCH): | Increment | When to use | Example | | --------- | ---------------------------------------------------- | ------------- | | **Patch** | Fixes and small rule tweaks | 1.4.2 → 1.4.3 | | **Minor** | New rules or decisions, existing behaviour unchanged | 1.4.3 → 1.5.0 | | **Major** | Changes that break how consumers call your decisions | 1.5.0 → 2.0.0 | ## Drafts and publishing A draft is a release without a version number. Use drafts to package work in progress, share a snapshot with reviewers, or stage something you plan to publish later. Drafts appear under the **Drafts** filter and carry a draft badge. To publish a draft, open **Publish…** from its row menu, pick the version increment, and confirm. The release gets the next semantic version and moves to the **Published** filter. ## Deploy a release Click a release to open the deploy view. Pick an environment to push this release to - the pre-flight checks at the top confirm tests and configuration are good to go. Each environment card shows its current state: the version that's live, "Idle - no release deployed yet." if nothing is deployed, or "This release is already live here." when the environment is already serving this release. Select an environment and click **Deploy**. Environments marked with a "requires review" chip don't deploy directly. For those, the button reads **Open review to deploy**: clicking it opens a deploy request, and the release goes live once the request is approved. See [Requests](/brms/review/requests). Deploying never changes the release itself. The same snapshot can be live on staging and production at once, and you can re-deploy any earlier release to roll back. ## Download a release Use the download button on a release row, or **Download ZIP** in the deploy view, to save the release as a ZIP archive. The archive contains the release's decision graphs and policies, so you can keep it for audit purposes or evaluate it outside the platform. ## Update or remove a release **Update…** lets you edit a release's name and description - useful for adding change notes after the fact. The snapshot itself stays untouched. **Remove…** deletes the release permanently. Removing a release cannot be undone. Stage environments serving this release will stop evaluating. Deployment environments keep their last pushed bundle and continue working until a new release is deployed. # Quality Control Source: https://docs.gorules.io/brms/quality/quality-control Run every test in the workspace, track coverage, and review static analysis findings in one place. Quality Control is the project's health dashboard. It runs every test in the workspace, shows which graphs have no coverage, and surfaces every static analysis finding - so you can see at a glance whether the branch is safe to ship. Open it from **Quality Control** in the sidebar's Quick Access. Quality Control page showing test files with pass/fail/skip counts, static analysis status, and untested graphs ## Running the suite Click **Run all** to execute every test case in every test file on the current branch. The Files table shows one row per [test file](/brms/build/testing) with its totals - pass, fail, skip - and expands to individual cases with their status and timing. Click **Export PDF** to produce a shareable report of the run - useful for sign-off, audits, or attaching to a release. ## Static analysis Every graph and policy on the branch is checked continuously for static issues - type errors, conflicting rules, dead logic. The page summarises how many documents are clean and how many have findings; expand a file to see each diagnostic and click it to jump straight to the offending node or block. See the [static analysis reference](/brms/quality/static-analysis) for every check and what it means. ## Untested graphs Graphs with no test coverage are listed by name. Add a `.test` file alongside a graph and it moves into the Files table automatically. Coverage here is deliberately simple: a graph either has tests exercising it or it doesn't. ## Quality Control and shipping The same signals appear as [pre-flight checks](/brms/review/review) whenever you save changes or [create a release](/brms/deploy/releases): tests, static analysis, and configuration are checked at the moment you publish. Quality Control is where you watch them continuously; pre-flight is where they meet your changes. # Static analysis Source: https://docs.gorules.io/brms/quality/static-analysis Reference for every check GoRules runs against your graphs and policies - and how to fix what it finds. GoRules continuously analyses every graph and policy as you edit - no run required. Findings appear in the document's **Problems** tab, on the [Quality Control](/brms/quality/quality-control) page, and as a [pre-flight check](/brms/review/review) when you save. Problems tab showing PREFER_DICTIONARY hints on decision table output columns, with explanations and suggested fixes Each finding has a severity: | Severity | Meaning | | -------- | ----------------------------------------------------------------------------------------------- | | Error | The document will misbehave or fail to evaluate. Fix before shipping. | | Warning | Likely a mistake, but evaluation still works. | | Hint | A quality suggestion. Hints appear in the Problems tab (toggleable) but not on Quality Control. | Static analysis needs to know your types. A graph whose input node has no schema - or an empty one - reports `MISSING_INPUT_SCHEMA` and skips deeper checks until you define one in the [Interface tab](/brms/build/graphs#interface). ## Names and types | Code | What it means | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `UNDEFINED_VARIABLE` | An expression references a property that nothing declares or computes. Usually a typo or a missing data model property. | | `TYPE_MISMATCH` | A value doesn't match the expected type - often an optional value used in arithmetic without a fallback, or a typed table cell with the wrong kind of value. | | `INVALID_EXPRESSION` | The expression doesn't evaluate against the resolved types. | | `PARSE_ERROR` | The expression isn't valid ZEN syntax. | | `IMPLICIT_ANY` | A schema leaf has no type, so it resolves to `any` and can't be checked. Give it a concrete type. | | `INVALID_NAME` | An identifier contains dots, brackets, or whitespace, or starts with a digit. | | `UNRESOLVED_FUNCTION_TYPE` | A function's return type can't be determined. | | `MAX_DEPTH_EXCEEDED` | Nesting exceeds the analyser's depth limit. | ## Writes and dependencies | Code | What it means | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `DUPLICATE_WRITER` | Two blocks write the same property. Checked across a policy's whole import group - the conflict can be in another file. | | `CYCLIC_DEPENDENCY` | Rules depend on each other in a loop, so no valid order exists. | | `SELF_REFERENCING_WRITE` | A block writes a property it also reads. | | `INPUT_OVERRIDE` | A rule overwrites a property declared as input. | | `INVALID_WRITE_PATH` | The write target isn't a valid property path. | | `PARTIAL_OBJECT_WRITE` | A block writes part of an object another block writes whole. | | `MIXED_SCOPE` | One block writes multiple entities, or mixes entity and global writes. Split it. | | `UNREACHABLE_ENTITY_READ` | A block reads an entity no execution path can populate. | ## Data models, dictionaries, and imports | Code | What it means | | --------------------------- | ----------------------------------------------------------------------------------------------------------- | | `DATA_MODEL_COLLISION` | The same entity is declared twice with different shapes, or an entity, global, and dictionary share a name. | | `UNKNOWN_DATA_MODEL_TARGET` | A reference points at a name that is neither an entity nor a dictionary. | | `DUPLICATE_PROPERTY` | A data model declares the same property twice. | | `DUPLICATE_ENUM_VALUE` | A dictionary contains the same value twice. | | `IMPORT_NOT_FOUND` | An imported policy path doesn't resolve to a document. | | `CIRCULAR_IMPORT` | Policies import each other in a cycle. | ## Structure and completeness | Code | What it means | | ------------------------------ | ---------------------------------------------------------------------------------------------------- | | `EMPTY_BLOCK` | A block has no content. An error on assertions; a warning on empty tables, matches, and expressions. | | `MISSING_DEFAULT_BRANCH` | A match has no branch for the remaining cases. | | `UNSUPPORTED_NESTED_ITERATION` | Iteration nested deeper than the engine supports. | | `INVALID_GRAPH_STRUCTURE` | The graph's nodes and edges don't form a valid flow. | | `UNREACHABLE_NODE` | A node no path from the input can reach. | | `MISSING_INPUT_SCHEMA` | The graph's input node has no usable schema; deeper checks are skipped. | | `UNCHECKED_NODE` | A node the analyser couldn't verify. | | `NULLABILITY_DIVERGENCE` | A value's nullability differs between paths that later join. | ## Quality hints Hints don't indicate bugs - they point at rules that could be simpler, safer, or easier to maintain. | Code | Suggestion | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `REDUNDANT_NULLISH` | A `??` fallback on a value that is never null - dead code; remove the fallback. | | `REPEATED_DERIVATION` | The same sub-expression is computed in several places. Compute it once and reference it. | | `PREFER_MATCH` | A construct that would read better as a match block. | | `PREFER_DICTIONARY` | An inline set of fixed values that should be a named [dictionary](/brms/build/policies#dictionaries) - you get membership checking and labels for free. | | `REDUNDANT_TABLE_ROW` | A decision table row that can never produce a distinct outcome. | | `NON_DISCRIMINATING_COLUMN` | A table column that never affects which row matches. | | `REDUNDANT_PARENTHESES` | Parentheses that don't change evaluation. | # Branches Source: https://docs.gorules.io/brms/review/branches Work on rule changes in isolation, browse history, compare versions, and resolve conflicts. A branch is an independent copy of your project's rules. Changes you save on a branch don't affect main - or anyone working on it - until you merge them. This lets you prepare larger changes over several sessions, try out ideas safely, and route work through review before it goes live. Every project starts with a main branch, which is the default. Your workspace always points at one branch, and everything you edit is a draft on top of it. ## Switching and creating branches The branch selector sits in the sidebar header. Click it to see all branches, search them, or switch to another one. Your unsaved draft stays with the branch it belongs to, so switching back later picks up where you left off. You create a branch from the Review page: choose the **New branch** card when saving, adjust the suggested name if you like, and click **Create branch & save**. Your changes land on the new branch, leaving main untouched. See [Review & save](/brms/review/review) for the full flow. ## History The History page, available from the sidebar rail, lists every commit on a branch - who saved it, when, and with what message. Use the branch picker to view another branch's history, and the search box to filter commits by message. History page showing a searchable list of commits with author, message, and timestamp Each commit offers a few actions: * **Inspect commit** opens the full set of files as they existed at that commit, so you can see exactly what a rule looked like at any point in time. * **Compare with…** opens the compare view with this commit as one side. * **Copy commit ID** copies the commit's identifier, useful when referencing a specific version with teammates. * **Revert to this commit** creates a new branch off the latest main commit with the contents of the selected commit applied. Nothing is overwritten - you review the revert branch and merge it like any other change. ## Comparing versions The compare view shows the differences between any two points in your project: pick a base and a target, each of which can be a branch, a release, or an individual commit. Graphs, tests, and policies display a visual diff, and you can switch to a source view for a line-by-line comparison. Use it to answer questions like "what changed between the March release and today?" before merging or deploying. ## Keeping a branch in sync When several people work on the same project, branches can drift apart. The BRMS surfaces each situation with a clear prompt, so you never have to guess what state you're in. ### Updates on the server If a teammate saved to your branch after you last synced, you'll see "Branch has updates on server". Click **Pull latest** to bring your local copy up to date. Pull before saving so your commit builds on the latest version. ### Conflicts with the remote branch If you and a teammate changed the same files, pulling shows "Conflicts with remote" and a **Resolve conflicts** action. This opens the Merge Workbench, which walks you through each conflicting file with two sides: Mine (your version) and Remote (the server's version). For each conflict, pick the side to keep with **Use Mine** or **Use Remote**, or apply one side everywhere with **Use Mine for all**. Toggle between the Visual and Code views to compare the two sides - the visual diff is available for graphs, tests, and policies. Once every conflict is resolved, apply the merge to finish syncing. ### Falling behind main While you work on a branch, main keeps moving. When that happens the Review page shows "Branch needs rebase" with how far behind you are, such as "Branch is 4 commits behind main". Click **Pull from main** to bring those commits into your branch. Save your draft first - rebasing refreshes your local state. If your branch changed the same files as main, the prompt shows a conflict count instead. Click **Resolve & rebase** to open the Merge Workbench, this time with sides labelled Mine and Main. Resolve each conflict, then apply the rebase. A branch must be up to date with main before it can be merged. ### Rewritten branch history Occasionally a branch's history is rewritten on the server - for example, after a revert. If you have no local edits, the BRMS syncs quietly. If you do, a "Branch history has changed" alert appears with two choices: **Save local changes** re-applies your edits on top of the new history, and **Reset to remote** discards them in favour of the server's version. ## Merging a branch When your branch is ready, open [Review & save](/brms/review/review) and either merge to main directly or create a merge request for approval. Merge requests are covered in [Requests](/brms/review/requests). After merging, the changes are eligible for a release - see [Releases](/brms/deploy/releases). # Requests Source: https://docs.gorules.io/brms/review/requests Route merges, deployments, and publishing through team review and approval. Requests let your team review important actions before they happen. Instead of merging a branch or deploying a release on the spot, you open a request; teammates review the proposed change, discuss it, and approve or reject it. Once the requirements are met, the request can be completed and the action goes through. Whether an action needs a request depends on your project's settings - see [Approval requirements](#approval-requirements) below. ## The Requests page Open Requests from the sidebar rail. A badge on the icon shows how many requests are pending. Filter the list by Pending, Completed, Cancelled, or Rejected, or search by name. Requests page showing a filtered list of pending requests and an open request with its discussion timeline Requests cover five kinds of action: | Type | Example title | What completing it does | | --------- | ------------------------------- | -------------------------------------- | | Merge | Merge pricing-update to main | Merges the branch's commits into main. | | Deploy | Deploy v1.4.0 to Production | Pushes a release to an environment. | | Undeploy | Undeploy v1.4.0 from Production | Removes a release from an environment. | | Publish | Publish Shipping Fees | Publishes a decision. | | Unpublish | Unpublish Shipping Fees | Unpublishes a decision. | Merge requests are opened from the Review page - see [Review & save](/brms/review/review). Deploy requests open automatically when you deploy a release to an environment marked "requires review" - see [Releases](/brms/deploy/releases). ## Inside a request Selecting a request opens its detail pane. The header shows the essentials: Author, Branch, Type, Status, Approvals (for example "1/2 approval groups"), and when it was opened. For merge requests you can also browse the proposed file changes, shown as a diff against main. ### Discussion The Discussion section is a timeline of everything that happened on the request, oldest first: who opened it, pushed changes, approved, withdrew approval, rejected it, completed it, or cancelled it - along with any comments. The composer at the bottom offers four actions: **Comment**, **Approve**, **Reject**, and **Withdraw** (which takes back your earlier approval). Authors can't approve or reject their own request. Every request needs a second pair of eyes. ### Completing a request The completion block at the bottom of the pane tells you what's left: | State | Meaning | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Ready to complete | All conditions are met. Click **Complete request** to carry out the action. | | Awaiting approvals | Required approvals haven't come in yet. | | Rebase required before completion | Main has moved on since the request was opened. Click **Rebase** to bring the branch up to date; if there are conflicts, you'll resolve them as described in [Branches](/brms/review/branches#keeping-a-branch-in-sync). | | Nothing to merge | The branch has no changes compared to main. Push new changes or cancel the request. | You can abandon a request at any time with **Cancel request**. Cancelling leaves the branch and its commits intact - only the request is closed. ## Approval requirements Project settings define what a request needs before it can be completed: * No approvals: actions go through directly, without a request. * Request without approvals: a request is created for visibility, and anyone can complete it. * Any one group: one approval from any of the project's approval groups is enough. * One per group: each approval group must provide at least one approval. Environments add their own layer: marking an environment as "requires review" means every deployment to it opens a deploy request, regardless of how it was triggered. Pair approval requirements with a protected main branch. Contributors then save their work to branches, and every change reaches main through a reviewed merge request. Pre-flight checks on the [Review page](/brms/review/review) and tests from [Quality Control](/brms/quality/quality-control) give reviewers the signal they need to approve with confidence. # Review & save Source: https://docs.gorules.io/brms/review/review Review your changes, run pre-flight checks, and save them to a branch. Your project workspace is a draft. Edits you make stay on your device until you save them, so you can experiment freely without affecting anyone else. When you're ready to make your work permanent, the Review page shows you exactly what changed, checks that everything is in good shape, and saves your changes to a branch. To open it, click the black **Review** button in the sidebar. Review page showing the Ready to publish heading, the pre-flight checks card, and the Where to publish options ## What you'll see The heading at the top tells you where you stand: | Heading | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------------- | | Ready to publish | You have unsaved changes, listed below with a summary such as "3 changes ready · 1 added · 2 modified". | | Nothing to publish | You haven't changed anything since the last save. | | All caught up | Everything is saved and your branch is in sync. | Below the heading, each changed file appears with its type of change: added, modified, deleted, or renamed. Review the list before saving - if something shouldn't be there, you can revert it from the Changes panel in the workspace sidebar. ## Pre-flight checks The Pre-flight card runs a series of checks on your draft. Each check reports one of four results: passing, warning, failing, or informational. The card headline summarises the worst result - from "All checks have passed" through "Pre-flight has warnings" to "Issues need attention". ### Sync Confirms your draft is up to date with the server. "Working tree in sync with remote" means you're good to go. If a teammate saved to the same branch first, you'll see "Remote has newer commits" with a **Pull remote** action - pull before saving to avoid losing work. If your changes overlap with theirs, the check shows "Conflicts detected with remote" and tracks how many you've resolved. Once every conflict is settled, it reads "Conflicts resolved" and offers **Apply merge**. See [Branches](/brms/review/branches) for how conflict resolution works. ### Tests Runs the test cases on your branch and reports the result: "All 12 test cases passing", a failing count such as "3 failing cases", "2 test files not yet run" if some haven't executed, or "No tests on this branch" if none exist. Learn how to add tests in [Testing](/brms/build/testing). ### Static analysis Scans your rules for structural problems, such as broken references or invalid expressions. It reports "Static analysis passing", or a count of errors or warnings. ### Environments and reviews Pre-flight also tells you how many deployment environments are available (or warns "No deployment environments configured"), flags any open reviews on the branch, and shows the previous published version for context. Failing tests or static analysis errors turn the check red, but they don't stop you from saving. This lets you save work in progress. The states that do block saving are an out-of-sync or conflicted draft, and branch protection on the target branch. ## Where to publish On the default branch, the "Where to publish" section offers two cards: * **Save to main** commits your changes directly to the active branch. Use this for changes that don't need review, on projects where main isn't protected. * **New branch** branches off the current commit and saves your changes there. The BRMS suggests a branch name, which you can edit. Click **Create branch & save** to confirm. The commit message field is pre-filled with a summary of your changes, such as "Update Pricing and 3 more". Replace it with something more descriptive if the summary doesn't capture the intent. If main is protected, direct saves are unavailable and you'll see a notice that approvals are required. Save to a new branch and open a review instead - the [Requests](/brms/review/requests) page walks you through the approval flow. ## Saving on a branch On any branch other than the default, saving commits your changes to that branch. Once the branch has commits ahead of main, the Review page offers two ways forward: * **Create merge request** opens a request so teammates can review and approve the changes before they reach main. The request appears on the [Requests](/brms/review/requests) page. * **Merge to main** merges directly. This is available when the project doesn't require approvals and the branch is up to date with main. If main has moved ahead of your branch, you'll be asked to pull from main first - see [Branches](/brms/review/branches#keeping-a-branch-in-sync) for details. Once your changes are on main, you can package them into a release and deploy them. See [Releases](/brms/deploy/releases). # Deployments Source: https://docs.gorules.io/brms/setup/deployments Configure cloud storage providers for deploying your decision rules. Deployments connect your GoRules projects to cloud storage, enabling you to publish rules for consumption by your applications. ## Supported providers | Provider | Description | | -------------------- | ----------------------------- | | AWS S3 | Amazon Simple Storage Service | | Azure Storage | Microsoft Azure Blob Storage | | Google Cloud Storage | Google Cloud Platform storage | ## Creating a deployment 1. Go to Organisation settings → Deployments 2. Click "Add deployment" 3. Enter deployment details: * **Name**: Descriptive name (e.g., "Production S3", "Staging Azure") * **Description**: Optional details about the deployment 4. Select a provider and configure credentials ### AWS S3 configuration | Field | Description | | ---------------------- | ----------------------------------------------------------- | | Authenticate using IAM | Use instance role instead of credentials | | Region | AWS region (e.g., `us-east-1`) | | Bucket | S3 bucket name | | Endpoint | Custom endpoint for S3-compatible services (optional) | | Path | Subdirectory within bucket (optional) | | Force path style | Enable for S3-compatible services requiring path-style URLs | ### Azure Storage configuration | Field | Description | | ---------------------- | ----------------------------------------------------- | | Authenticate using IAM | Use managed identity | | Blob service URL | Format: `https://{accountName}.blob.core.windows.net` | | Container | Blob container name | | Path | Virtual directory path (optional) | ### Google Cloud Storage configuration | Field | Description | | ---------------------- | --------------------------------------- | | Authenticate using IAM | Use service account attached to compute | | Bucket | GCS bucket name | | Path | Object prefix (optional) | ## IAM authentication All providers support IAM-based authentication, eliminating the need to store credentials in GoRules: **AWS**: Attach an IAM role to your GoRules compute instance with S3 permissions. **Azure**: Enable managed identity and grant Storage Blob Data Contributor role. **GCS**: Attach a service account with Storage Object Admin permissions. # Git Sync Source: https://docs.gorules.io/brms/setup/git-sync Automatically synchronize your project decisions to a Git repository. Git Sync automatically creates a Pull Request (GitHub, Azure DevOps) or Merge Request (GitLab) in your repository whenever you merge branches or commit to the main branch in BRMS. This keeps your Git repository in sync with your decision files. ## Prerequisites Before enabling Git Sync on a project, an organization administrator must connect a Git provider. For self-hosted deployments, the provider must first be configured at the server level - see [Git integrations](/developers/deployment/brms/integrations). ## Connect a Git provider Organization administrators can connect GitHub, GitLab or Azure DevOps from the integrations page. 1. Navigate to **Admin > Integrations** 2. Find the card for your preferred provider (GitHub, GitLab or Azure DevOps) 3. Click **Connect** ### Connecting GitHub 1. Click **Connect to GitHub** 2. If the GitHub App is already installed on your account, select which organization to connect 3. If not installed, you'll be redirected to install the GitHub App - choose which repositories to grant access to 4. Complete the authorization ### Connecting GitLab 1. Fill in the connection form: | Field | Description | | ------------------ | -------------------------------------------- | | **GitLab URL** | `https://gitlab.com` or your self-hosted URL | | **Application ID** | From your GitLab OAuth application | | **Secret** | From your GitLab OAuth application | 2. Click **Connect to GitLab** 3. Authorize the application in GitLab 4. You'll be redirected back to BRMS ### Connecting Azure DevOps Connecting Azure DevOps lets BRMS push to Azure Repos and trigger Azure Pipelines from [webhooks](/brms/setup/webhooks). For the Entra ID app registration walkthrough and server-side details, see [Git integrations](/developers/deployment/brms/integrations#azure-devops-configuration). Two connection methods are available: | Method | When to use | | --------------------- | ---------------------------------------------------------------------------------------------------------- | | **Service Principal** | Recommended for Azure DevOps Services - a machine identity that does not expire with a user | | **Access Token** | A Personal Access Token. The only option for Azure DevOps Server, but tied to a user and subject to expiry | Both methods require the **Organization URL**: `https://dev.azure.com/` for Azure DevOps Services, or your collection URL for Azure DevOps Server. #### Service Principal 1. Register an application in Microsoft Entra ID and create a client secret 2. In Azure DevOps, go to **Organization settings > Users** and add the application as a member 3. Grant it access to the projects and repositories it should reach 4. In BRMS, fill in the connection form: | Field | Description | | --------------------------- | ------------------------------------------- | | **Directory (tenant) ID** | From your Entra ID tenant | | **Application (client) ID** | From the app registration | | **Client secret** | The secret created for the app registration | #### Access Token 1. Create a Personal Access Token in Azure DevOps (**User settings > Personal access tokens**) with the following scopes: * **Code**: Read & write * **Build**: Read & execute 2. Paste the token into the connection form and click **Connect** GitLab and Azure DevOps credentials are encrypted and stored securely using your organization's secrets provider. For self-hosted deployments, [secrets management](/developers/deployment/brms/secrets-management) must be configured before connecting these providers. Azure Repos creates repositories empty. Push an initial commit to the target repository before enabling Git Sync on it. ## Enable Git Sync on a project Once a Git provider is connected, project managers can enable Git Sync for individual projects. 1. Navigate to **Project Settings > Git Sync** 2. Toggle **Enable Git Sync** to on 3. Configure the sync settings: | Setting | Description | | ----------------- | ----------------------------------------------------- | | **Git Provider** | Select your connected provider | | **Repository** | Choose the target repository | | **Target Branch** | Branch to create PRs against (defaults to `main`) | | **Path** | Subdirectory for synced files (e.g., `rules/pricing`) | 4. Click **Save Configuration** You must specify a subdirectory path. Syncing to the repository root is not allowed. ## How Git Sync works When you merge a branch in BRMS: 1. A new branch is created in the Git repository (e.g., `commit-abc12345`) 2. All decision files from your project are committed to the configured path 3. A metadata file is included with sync information 4. A Pull Request or Merge Request is automatically created against your target branch ### Synced file structure ``` your-repository/ └── {configured-path}/ ├── .config/ │ └── project.json # Sync metadata ├── decision-table.json ├── rule-flow.json └── ... # All project decision files ``` ### Project metadata Each sync includes a `.config/project.json` file with metadata: ```json theme={null} { "version": "1", "project": { "id": "project-uuid", "key": "project-key" }, "commit": { "id": "commit-uuid", "message": "Merge branch feature into main", "branchName": "main" }, "sync": { "syncedAt": "2024-01-15T10:30:00.000Z" } } ``` ## Sync status After a merge, the commit history shows a sync status badge: | Badge | Status | Description | | ----- | ------- | ---------------------------------- | | Blue | Pending | Sync is in progress | | Green | #123 | PR/MR created (click to view) | | Red | Failed | Error occurred (hover for details) | ## Permissions | Action | Required permission | | -------------------- | -------------------------------- | | Connect Git provider | Organization Administrator | | Configure Git Sync | Project Manager (Manage Project) | | View sync status | Any project member | ## Troubleshooting ### "No Git integration configured" warning No Git provider is connected to your organization. Contact your organization administrator to configure a GitHub, GitLab or Azure DevOps integration in **Admin > Integrations**. ### Sync status shows "Failed" Hover over the red badge to see the error message. Common causes: * Repository access was revoked * Target branch no longer exists * Network connectivity issues ### Sync stays "Pending" for more than 5 minutes If a sync shows "Pending" for an extended period, it may have failed silently. The status will eventually change to "Sync timed out". Check your Git provider to see if a PR was partially created. ### Cannot see repositories in dropdown The connected Git provider account may not have access to the repository you're looking for. For GitHub, verify the GitHub App is installed on the organization that owns the repository. For GitLab, verify the connected user has access to the repository. For Azure DevOps, verify the service principal or token owner has access to the Azure DevOps project that contains the repository. # Organisation Settings Source: https://docs.gorules.io/brms/setup/organisation-settings Configure your organisation's branding, access controls, and display settings. Organisation settings control how your GoRules instance looks and who can access it. Access these settings from the gear icon → Organization settings. ## Sign up settings Control which email domains can join your organisation automatically. ### Configuring allowed domains 1. Go to Settings → Organisation 2. Expand "Sign up settings" 3. Enter allowed domains (comma-separated, e.g., `@gorules.io, @company.com`) 4. Click Update When no domains are specified, users cannot self-register and must be invited. ## Theme Customize your organisation's branding. ### Available options | Setting | Description | | ------------- | ------------------------------------------------ | | Logo URL | URL to your organisation's logo image | | Primary color | Main accent color (supports HEX and RGBA values) | ### Updating theme 1. Go to Settings → Organisation 2. Expand "Theme" 3. Enter your logo URL 4. Set primary color using HEX or RGBA values 5. Click Update Use the Reset button to restore default branding. ## Organisation name Set the display name shown throughout the interface. ### Changing the name 1. Go to Settings → Organisation 2. Expand "Update name" 3. Enter the new display name 4. Click Update # Project versions Source: https://docs.gorules.io/brms/setup/project-versions Understand the differences between v1 (Legacy) and v2 (Git-like) projects and how to migrate. GoRules supports two project versions. **v2 (Git-like)** is the actively developed version with full version control, branching, and all new platform features. **v1 (Legacy)** uses simple document-based storage and is no longer receiving new features. New projects default to v2. If you're still on v1, you'll see a banner in the BRMS: You are using Project v1 (Legacy). Migrate to v2 (Git-like) for branches, webhooks, git sync and more. ## Comparison | Capability | v1 (Legacy) | v2 (Git-like) | | ------------------------------------ | :---------: | :-----------: | | Decision authoring | Yes | Yes | | Simulator | Yes | Yes | | Releases and environments | Yes | Yes | | Change requests (release deployment) | Yes | Yes | | Change requests (model publish) | Yes | - | | Change requests (branch merge) | - | Yes | | Diff viewer (single model) | Limited | Yes | | Diff viewer (full branch) | - | Yes | | Conflict resolution | - | Yes | | Repository with commit history | - | Yes | | Branches | - | Yes | | [Git Sync](/brms/setup/git-sync) | - | Yes | | [Webhooks](/brms/setup/webhooks) | - | Yes | | [AI](/brms/build/ai) | Basic | Yes | | [MCP](/developers/mcp) | - | Yes | | Semantic versioning for releases | - | Yes | All new features are developed exclusively for v2 projects. ## What v2 adds ### Commit history v2 projects track every change with commits, authors, and timestamps - similar to Git. You can browse [history](/brms/review/branches#history), view diffs, and understand exactly what changed and when. ### Branches, requests, and conflict resolution [Branches](/brms/review/branches) let team members work on rule changes in isolation. When changes are ready, they can be merged directly or go through a [request](/brms/review/requests) workflow with configurable approval requirements. v2 also provides conflict resolution when merging branches with overlapping changes. Both v1 and v2 support requests for release deployment. In v1, change requests also apply when publishing a single model. In v2, merge requests apply when merging a branch into main - covering all changed files in one review. ### Diff viewer v1 includes a limited diff viewer for individual models. v2 extends this with a full branch-level diff viewer that shows all changes across every file in a branch, along with release comparison. ### Git Sync and webhooks v2 projects can synchronize decisions to an external Git repository via [Git Sync](/brms/setup/git-sync) and notify external systems of events via [Webhooks](/brms/setup/webhooks). ## Migrating from v1 to v2 You can create a new v2 project from an existing v1 project using the **Duplicate project** option in **Organisation Settings → Projects**. This copies all your decision files into a new v2 project. v1 projects are not automatically upgraded. Your existing v1 projects continue to work, but they will not receive new features. Migrate to v2 to take advantage of branching, change requests, and all future platform improvements. ## Creating a v2 project 1. Go to **Organisation Settings → Projects** 2. Click **Create project** 3. Enter a name and project key 4. Select **Git-like** as the project version 5. Click **Create** New projects default to Git-like (v2). See [Projects](/brms/setup/projects) for more details. # Projects Source: https://docs.gorules.io/brms/setup/projects Create and manage decision projects with version control. Projects are containers for your business rules and decision logic. Each project has its own repository, branches, releases, and team access. ## Project types GoRules supports two project versions: | Version | Description | | ------------- | --------------------------------------------------------------------------- | | Git-like (v2) | Full version control with branches, merge requests, and semantic versioning | | Standard (v1) | Simple document-based storage | New projects default to Git-like for better collaboration and change tracking. ## Creating a project 1. Go to Organisation settings → Projects 2. Click "Create project" 3. Enter project details: * **Name**: Display name for the project * **Project key**: URL-friendly identifier (auto-generated from name) * **Project version**: Git-like (recommended) or Standard 4. Click Create ### Duplicating projects Use "Duplicate project" to create a copy with all rules and configuration. ## Project list The projects page shows all organisation projects with: | Column | Description | | ------------ | ------------------------------------ | | Name | Project name with link to repository | | Admin | Project administrator | | Last updated | Most recent modification time | | Actions | Open project or manage settings | ### Filtering Toggle "Deleted only" to view soft-deleted projects for recovery. ## Managing a project Click "Manage" on any project to access: ### Members tab Control project-level access: 1. Click "Invite member" 2. Select users from your organisation 3. Assign to groups for permission control ### Groups tab Create project-specific permission groups: | Field | Description | | ---------------- | ----------------------------- | | Name | Group identifier | | Description | Group purpose | | Permissions | Selected from permission tree | | File Permissions | Path-based access rules | Groups work independently from organisation-wide roles, allowing fine-grained project access. # Users & Roles Source: https://docs.gorules.io/brms/setup/users-roles Manage team members, define roles with granular permissions, and handle invitations. Users & Roles lets you control who has access to your organisation and what they can do. Access this section from Organisation settings → Users & Roles. ## Users View and manage all users in your organisation. ### User information | Column | Description | | ------- | ----------------------------------------- | | Name | User's email or display name | | Type | User type (Self, Member, Service Account) | | Status | Account status (active, pending) | | Roles | Assigned organisation-wide roles | | Actions | Edit or remove user | ### Inviting users 1. Click "Invite users" 2. Enter email addresses (multiple supported) 3. Click Invite Invited users receive an email with instructions to join. ### Service accounts Service accounts enable programmatic API access without user credentials. 1. Click "Create service account" 2. Configure the account name and permissions 3. Store the generated credentials securely ## Roles Roles define what actions users can perform across projects. ### Creating a role 1. Go to Users & Roles → Roles tab 2. Click "Create role" 3. Configure the role settings ### Role configuration | Field | Description | | ------------ | --------------------------------------------------------- | | Name | Role identifier | | Description | Purpose of the role | | IdP Groups | SSO/SAML group mappings (e.g., `TECH-leads, BA_analysts`) | | Users | Assign specific users to this role | | All projects | Toggle to apply role to all projects | | Projects | Select specific projects (when "All projects" is off) | ### Permission categories **Project-level permissions:** | Permission | Description | | -------------- | ------------------------------------------------- | | Project Admin | Full project access | | Project Manage | Members, groups, tokens, approvers, configuration | **Documents (v1 projects):** | Permission | Description | | ------------ | ---------------------------------------------------------- | | Full | Create, update, delete, move, copy, restore, graph content | | Edit Content | Update graph content | | View Content | View graph content only | | Edit View | Update view content only | **Branches (v2 projects):** | Permission | Description | | ---------- | ------------------- | | Create | Create new branches | | Merge | Merge branches | | Delete | Delete branches | **Releases:** | Permission | Description | | ---------- | ------------------------------- | | Manage | Create and edit releases | | Deploy | Deploy releases to environments | | Delete | Remove releases | **Environments:** | Permission | Description | | ---------- | ----------------------------------- | | Manage | Create, update, configure approvers | | Delete | Remove environments | ### File permissions Add granular permissions for specific files or paths within projects. ## Invitations Track pending and accepted invitations. ### Pending tab View invitations awaiting user acceptance. Resend or cancel invitations as needed. ### Accepted tab View historical record of accepted invitations. # Webhooks Source: https://docs.gorules.io/brms/setup/webhooks Automate workflows by sending event notifications to external services. This feature is available on eligible plans. Webhooks allow you to notify external systems when events occur in your GoRules project. Configure webhooks to trigger CI/CD pipelines, send notifications, or integrate with third-party services when commits are made, releases are created, or change requests are updated. ## Webhook list The Webhooks page displays all configured webhooks for your project. Access it from Settings → Webhooks. ### List columns | Column | Description | | ----------- | ---------------------------------------- | | Description | Webhook name or description | | Target | Destination URL or repository | | Events | Number of events the webhook listens to | | Status | Current state (active or inactive) | | Actions | View logs, update, or delete the webhook | ## Webhook types GoRules supports four webhook types for different integration scenarios. ### REST Webhook Send HTTP POST requests to any URL when events occur. Ideal for custom integrations, notification services, or internal APIs. **Configuration:** * URL: The endpoint that receives webhook payloads * Signing secret: Generated automatically for payload verification ### GitHub Trigger GitHub Actions workflows directly from GoRules events. Requires a configured GitHub integration. **Configuration:** * Repository: Select from connected GitHub repositories * Target Branch: Branch to dispatch the workflow on (e.g., `main`) * Workflow ID: The workflow filename or ID (e.g., `deploy.yml` or `12345678`) ### GitLab Trigger GitLab CI/CD pipelines from GoRules events. Requires a configured GitLab integration. **Configuration:** * Repository: Select from connected GitLab repositories * Target Branch: Branch to run the pipeline on (e.g., `main`) ### Azure DevOps Queue Azure Pipelines runs from GoRules events. Requires a configured Azure DevOps integration. **Configuration:** * Repository: Select from connected repositories (`project/repository`) * Target Branch: Branch to run against (e.g., `main`) * Pipeline: The pipeline to queue The Azure pipeline must declare a queue-time-overridable `GRL_PAYLOAD` variable, or the queue request is rejected on newer organizations. See the [Azure DevOps CI/CD guide](/developers/cicd/azure-devops#2-declare-the-queue-time-variable). GitHub, GitLab, and Azure DevOps webhooks require integrations to be configured first. Click "Configure Integrations" in the webhook form to set up the connection. For end-to-end pipeline examples - pulling the rules artifact and publishing it to object storage - see the CI/CD integration guides for [GitHub Actions](/developers/cicd/github-actions), [GitLab CI](/developers/cicd/gitlab-ci), and [Azure DevOps](/developers/cicd/azure-devops). ## Available events Select which events trigger the webhook. ### Commits (main branch) | Event | Description | | -------------- | --------------------------------------- | | Commit Created | A new commit is made to the main branch | ### Releases | Event | Description | | ----------------------- | --------------------------------------- | | Release Created | A new release is created | | Draft Release Published | A draft release is published | | Release Deployed | A release is deployed to an environment | ### Change Requests | Event | Description | | ------------------------ | -------------------------------------- | | Change Request Opened | A new change request is created | | Change Request Updated | A change request is modified | | Change Request Completed | A change request is merged | | Change Request Cancelled | A change request is abandoned | | Change Request Comment | A comment is added to a change request | ## Creating a webhook 1. Go to Settings → Webhooks 2. Click "Create webhook" 3. Enter a description 4. Select the webhook type (REST Webhook, GitHub, GitLab, or Azure DevOps) 5. Configure the target: * For REST: Enter the destination URL * For GitHub/GitLab: Select repository and workflow details 6. Select the events to listen for 7. Click "Create" For REST webhooks, a signing secret is generated and displayed once after creation. Copy it immediately as it won't be shown again. ## Signing secret REST webhooks include a signing secret for verifying payload authenticity. The secret is used to generate an HMAC signature sent with each request. ## Testing webhooks Before relying on a webhook in production, send a test request. 1. Open the create or update webhook form 2. In the "Test Webhook" section, select an event type 3. Click "Send Test" The test uses a sample payload with the test secret `grl_whsec_test` to verify your endpoint is reachable. ## Webhook logs View the history of webhook deliveries and troubleshoot issues. ### Accessing logs 1. Go to Settings → Webhooks 2. Click "View Logs" on the webhook row ### Log details Each log entry shows: | Field | Description | | ------------ | ------------------------------------- | | Event Type | The event that triggered the delivery | | Status | SUCCESS or failure status | | Attempted At | Timestamp of the delivery attempt | ### Expanded log view Click a log entry to see detailed information: **Request tab:** * URL: The destination endpoint * Payload: JSON data sent to the endpoint * Headers: Request headers including authentication **Response tab:** * Status code: HTTP response code (e.g., 204) * Body: Response content from the endpoint * Completion time: Request duration in milliseconds ### Retrying failed deliveries If a delivery fails, click "Retry" to resend the webhook with the same payload. ## Managing webhooks ### Updating a webhook 1. Click the three-dot menu on the webhook row 2. Select "Update" 3. Modify the configuration 4. Click "Update" You can change the description, events, and target configuration. The webhook type cannot be changed after creation. ### Enabling or disabling Toggle the "Active" switch in the update form to enable or disable a webhook without deleting it. ### Deleting a webhook 1. Click the three-dot menu on the webhook row 2. Select "Delete" 3. Confirm the deletion Deleting a webhook is permanent. Any external systems relying on the webhook will stop receiving notifications. ## Webhook payload Webhook payloads include event details in JSON format. ### Payload structure ```json theme={null} { "eventId": "dea48f7b-2cca-4113-a00d-eb8c0fb06a5d", "eventType": "release.created", ... } ``` The payload content varies by event type and includes relevant data such as commit information, release details, or change request metadata. ### Request headers | Header | Value | | --------------- | ---------------------------------- | | Content-Type | application/json | | User-Agent | GoRules-Webhook/1.0 | | X-Webhook-Id | Unique webhook identifier | | X-Webhook-Event | Event type (e.g., release.created) | | Authorization | Bearer token (for GitHub/GitLab) | ## Verifying webhook signatures REST webhooks include a signing secret for verifying payload authenticity. Each request contains an `X-Webhook-Signature` header with an HMAC-SHA256 signature of the request body. ### Signature format The signature header uses the format `sha256=`. Verify this against your own HMAC calculation using the webhook's signing secret. ### Node.js example ```javascript theme={null} const crypto = require('crypto'); const WEBHOOK_SECRET = 'your_webhook_secret'; function verifyWebhookSignature(receivedSignature, rawBody, secret) { if (!receivedSignature || !rawBody || !secret) { return false; } try { // Extract the hash part from 'sha256=' format const signatureHash = receivedSignature.split('=')[1] || receivedSignature; const hmac = crypto.createHmac('sha256', secret); hmac.update(rawBody); const expectedSignature = hmac.digest('hex'); // Use timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signatureHash, 'hex'), Buffer.from(expectedSignature, 'hex') ); } catch (error) { console.error('Signature verification error:', error); return false; } } // Express route handler router.post('/webhook', async (req, res) => { const signature = req.headers['x-webhook-signature']; const rawBody = req.rawBody; // Verify signature if (!verifyWebhookSignature(signature, rawBody, WEBHOOK_SECRET)) { return res.status(401).send('Unauthorized'); } // Process webhook const payload = req.body; console.log('Valid webhook:', payload.eventType); res.status(200).send('OK'); }); ``` You must use the raw request body for signature verification, not the parsed JSON. If you're using body-parser middleware, configure it to preserve the raw body: ```javascript theme={null} app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString(); } })); ``` ### Verification steps 1. Extract the `X-Webhook-Signature` header from the request 2. Get the raw request body (before JSON parsing) 3. Compute HMAC-SHA256 of the raw body using your signing secret 4. Compare the computed hash with the received signature using a timing-safe comparison 5. Reject the request if signatures don't match # Product updates Source: https://docs.gorules.io/changelog New releases and improvements for GoRules BRMS and Agent ## Features * **Per-Node Trace Inspection**: Test results in the graph editor now break down execution node by node. Pick any node to inspect its input, output, and a step-by-step trace showing which rows matched and which values were read and written, with the selected node highlighted on the canvas. See [Testing](/brms/build/testing). * **JSON View Toggle**: The Input and Output tabs in the tests panel can now switch between the visual tree and raw JSON. ## Fixes * **Graph Diff and Review Views**: Fixed a crash that showed a blank screen when opening diff, review, or merge views for graphs edited in the new graph editor. * **AI Chat Streaming**: The AI assistant stream now sends keepalive signals, preventing dropped responses behind proxies and load balancers. * **Rules API on Read-Only Filesystems**: The sandboxed evaluation runtime now loads fully in memory, fixing Rules API evaluations on deployments with a read-only filesystem. * **GitLab Webhook Dialog**: Fixed editing existing GitLab webhooks in project settings. * **Security Patches**: Upgraded dependencies to address vulnerabilities. ## First stable release of the ZEN Engine 2.0 line The open-source engine that powers GoRules is out of beta, with all SDKs released at 2.0.0: [Rust](/developers/sdks/rust), [Node.js](/developers/sdks/nodejs), [Python](/developers/sdks/python), [Go](/developers/sdks/go), [Java](/developers/sdks/java), [Kotlin](/developers/sdks/kotlin), [C#](/developers/sdks/csharp), [iOS](/developers/sdks/ios), and [Android](/developers/sdks/android). * **Per-column collect**: decision table output fields ending in `[]` collect values across all matching rows while the rest of the table stays first-hit. * **Unified loaders**: static, filesystem, and zip loader configurations with pre-compilation at construction, consistent across every SDK. * **Batch evaluation**: evaluate many requests in one call with per-item success envelopes on all SDKs. * **Hardened runtime**: out-of-range numbers return errors instead of crashing, division and modulo by zero return `null`, and arithmetic overflow reports an error. * **Go v2 module**: the Go SDK moves to `github.com/gorules/zen-go/v2` with typed loader configurations and batch support. Upgrading from 0.x? Rust users opting into arbitrary-precision numbers must enable the `arbitrary_precision` feature explicitly; Go users update the import path and wrap callback loaders with `zen.Loader(...)`. ## Features * **Azure DevOps Integration**: Push decision files to Azure Repos with [Git Sync](/brms/setup/git-sync) and trigger Azure Pipelines from webhooks. * **Sandboxed Evaluation**: Rules API evaluations now run in an isolated WebAssembly sandbox with strict time and memory limits. * **Rules Sync**: A new API for keeping deployed rules in sync with BRMS, starting with the new [`gorules pull`](/developers/cli) command for shipping rule artifacts from CI/CD. * **Project OpenAPI Schema**: `GET /api/rules/` on BRMS and the Agent returns an OpenAPI document for your rules, with input and output schemas inferred from your models. See [Developer tools](/developers/developer-tools). ## Fixes * **Graph Editor Refinements**: Polished the redesigned graph editing experience with styling and stability fixes. * **Security Patches**: Upgraded dependencies to address vulnerabilities. ## GoRules 2.0 is here Our biggest release to date. GoRules 2.0 introduces a redesigned, **AI-first experience**, a new way to write rules with **Policies**, an all-new editing experience for your **Graphs**, and a platform that understands your rules deeply enough to explain them in **natural language** and verify them before they ever run. Alongside 2.0, GoRules Cloud launches in dedicated commercial regions - [**US1 (East)**](https://us1.gorules.io) and [**EU1 (Europe)**](https://eu1.gorules.io) - with regional data residency. GoRules 2.0 is a **drop-in upgrade** - your existing projects and rules continue to work without any migration steps. ## Major Features * **New Experience**: A redesigned, AI-first workspace. Every part of your project lives in a clean, document-style interface that you can navigate, search, and edit in place - built for working side by side with AI. * **Policies**: A new rule type built on strongly typed models. You write policies in a descriptive document format where each rule reads as a clear statement of intent. The engine resolves them using a backward-chaining algorithm: you declare what must be true, and the engine determines how to evaluate it. * **Static Code Analysis**: Rules are analyzed as you write them. Compile-time checks against your typed models catch type mismatches, missing fields, and invalid references before deployment. * **Dictionaries**: Define reusable sets of predefined values once and reference them across your models and rules, keeping terminology consistent everywhere. * **Natural Language**: A new layer on top of expressions that presents your rules in plain language. Business users can read and verify logic without interpreting expression syntax. * **Smarter AI Agent**: The AI agent has been reworked to be significantly faster, more efficient, and smarter - with a deeper understanding of your models, rules, and project context. * **Reworked Graphs**: An all-new editing experience for graphs, with smoother navigation, faster editing, and a more intuitive canvas. Your graphs stay exactly the same - only the way you work with them is new. ## Platform * **Dev Tools**: A new section that exposes all rule evaluation through an OpenAPI specification, so you can explore integration points and capabilities before wiring them into your applications. * **New Evaluation Endpoints**: New eval endpoints for running your rules with improved performance and consistency across deployment targets. * **Localization**: GoRules is now available in multiple languages. * **Dark Mode**: Full dark mode support across the entire interface. ## Features * **Experience V2**: Introduced Experience v2. ## Fixes * **AI Assistant Crash**: Fixed an AI assistant crash when creating a release. * **AI Chat Scroll**: Reworked AI assistant chat scrolling behavior. * **CI/CD Optimisation**: Improved CI/CD pipeline performance for faster builds. * **Security Patches**: Upgraded dependencies to address vulnerabilities. ## Fixes * **CI/CD Optimisation**: Parallelized dev and prod region deployments and merged Lambda into the ECS pipeline. * **EU1 Production Deployment**: Added eu1 production region deployment. * **Security Patches**: Upgraded dependencies to address vulnerabilities. ## Fixes * **Static Asset Cache Control**: Fixed cache-control headers for static assets served by the API. * **API and Static Cache Headers**: Set appropriate cache-control headers on API and static responses. * **Lambda Alias on Deploy**: Fixed Lambda alias not updating on deployment. * **Release Deploy Permissions**: Fixed deploy release permissions in Projects v2. * **Security Patches**: Upgraded dependencies to address vulnerabilities. ## Features * **Reworked Settings Page**: Settings have been redesigned and are now presented in a dialog for a faster, more focused configuration experience. ## Fixes * **Committing Many Files**: Fixed an issue where commits containing many files would fail. * **Upload Folder Paths**: Fixed folder and release uploads in the UI editor not respecting paths. ## Features * **GoRules CI/CD Enhancements**: Various improvements to the CI/CD pipeline for more reliable builds and deployments. ## Fixes * **OpenAI Base URL**: Added `LLM_BASE_URL` environment variable to support custom OpenAI-compatible endpoints. See [AI setup](/developers/deployment/brms/ai-setup) for configuration details. * **MCP Chrome Permissions**: MCP connection now explicitly asks for Chrome permissions before connecting. ## Fixes * **Dependency Upgrades**: Upgraded dependencies to address vulnerabilities and improve stability. * **GoRules AI - Gemini Thinking Level**: Added thinking level low support for Gemini 2.5 models. * **GoRules AI - Document Upload**: Fixed document upload failure caused by AI SDK changes. * **UI Panel Fixes**: Resolved issues with panels not rendering correctly. ## Fixes * **GoRules AI - Simulation Without Graph**: Fixed an issue where AI simulation was not working without a graph opened. * **Datadog CI Upload**: Fixed source map upload in Datadog CI integration. * **AI SDK Update**: Updated AI SDK packages to fix OpenAI models not working. ## Features * **Lambda Deployment Healthcheck**: Added healthcheck logic to CI/CD pipeline to verify serverless deployments are running successfully. ## Fixes * **GoRules AI - Clear Chat**: Fixed clear chat button visibility in the AI assistant. ## Features * **Google Vertex AI Support**: Added Google Vertex AI as a new LLM provider for GoRules AI. Supports Gemini models with IAM-based authentication. See [AI setup](/developers/deployment/brms/ai-setup) for configuration details. ## Fixes * **User Invite Email Comparison**: Fixed user invite to use case insensitive email comparison. * **Swagger External Dependency**: Fixed swagger external dependency in Lambda deployments. * **View Setup Guide**: Fixed setup guide page not found issue. * **ZEN Engine Update**: Upgraded to the latest ZEN Engine, introducing `merge` and `mergeDeep` built-in functions. See [Built-in functions](/learn/zen-language/functions) for details. ## Fixes * **Auth Email Comparison**: Fixed authentication to use case insensitive email comparison. ## Fixes * **Helm Charts LLM Config**: Updated Helm charts with LLM configuration support. * **Commits Navigation**: Fixed clicking commits to correctly open the commits list page. ## Fixes * **GoRules AI - Amazon Bedrock Caching**: Fixed prompt caching support for Amazon Bedrock Anthropic models, reducing token usage and improving response times. ## Fixes * **GoRules AI - Anthropic Caching**: Fixed an issue where tokens were not being cached properly for Anthropic direct provider. ## Fixes * **Decision Table Performance**: Improved decision table performance for large tables. * **GoRules AI Auto Compacting**: Fixed an issue where auto compacting could enter a loop and produce console errors. ## GoRules AI is here This release introduces AI-powered rule authoring, MCP integration, and a built-in testing framework - bringing a new level of speed and intelligence to how you build decisions. ## Major Features * **GoRules AI - Write decisions using AI**: Describe what you need in natural language or files and the AI builds or modifies your decision graphs for you. Create decision tables, expressions, and entire graphs without manual configuration. Requires an LLM provider to be configured. See [AI assistant](/brms/build/ai) for usage and setup. * **GoRules MCP - AI editor integration**: Connect external AI assistants (Claude, Cursor, and others) to your GoRules projects through the Model Context Protocol. AI tools can read, modify, simulate, and test your decision graphs directly from your editor. See [MCP integration](/developers/mcp) for details. * **Testing framework**: Create, manage, and run test cases for your decision graphs with expected outcomes - or ask the AI to generate them for you. See [Test with simulator](/learn/authoring/testing) for writing tests and [AI assistant](/brms/build/ai#manage-test-cases) for AI-generated tests. ## Features * **Emergency Admin Override**: Added `EMERGENCY_ADMIN_EMAILS` environment variable for self-hosted deployments. Accepts a comma-separated list of email addresses that are automatically promoted to admin on login. This provides a recovery path when existing admins leave the organisation, allowing designated users to regain administrative access without direct database changes. * **Releases from Branches**: Releases can now be created from non-main branches. These branch-based releases are created as drafts using the latest branch commit and cannot be published with semantic versioning, but can be deployed to environments. This allows teams to test and validate changes in target environments before merging to main. ## Fixes * **Webhook Triggering**: Fixed webhook triggering on release deploy from change request. * **Merge Action**: Added merge to main on repository page. ## Features * **Webhooks**: Notify external systems when events occur in your project. Configure webhooks to trigger CI/CD pipelines, send notifications, or integrate with third-party services. Supports three webhook types: REST webhooks with HMAC-SHA256 signature verification, GitHub Actions triggers, and GitLab CI/CD pipeline triggers. Listen for commits, releases, and change request events. Includes delivery logs, retry functionality for failed deliveries, and test webhook capability. See [Webhooks](/brms/setup/webhooks) for setup instructions. This feature is available on Enterprise plan only. ## Features * **Git Sync**: Automatically synchronize your project decisions to a Git repository. When you merge branches or commit to main, BRMS creates a Pull Request (GitHub) or Merge Request (GitLab) with all your decision files. Supports both GitHub and GitLab, including self-hosted GitLab instances. See [Git Sync](/brms/setup/git-sync) for setup instructions. * **Secrets Manager**: Secure storage for sensitive credentials using envelope encryption with KEK/DEK hierarchy. Supports AWS KMS, Azure Key Vault, GCP KMS, or environment variable-based encryption. Each organisation has isolated encryption keys. See [Secrets management](/developers/deployment/brms/secrets-management) for configuration. ## Fixes * **Pull from Remote (Projects v2)**: Fixed an issue where pulling from remote could lose local uncommitted changes. The working copy now properly resolves local and remote changes. * **Direct Commit Enforcement (Projects v2)**: Fixed an issue where direct commit to main branch restrictions could be enforced without proper license validation. This enforcement now correctly validates the license tier. ## Features * **Azure Blob Storage for Commits and Releases**: Commits and releases can now be stored in Azure Blob Storage for improved scalability. This feature is available on Cloud and Azure deployments only. ## Fixes * **HTTP IAM on Cloud**: Disabled HTTP IAM on Cloud deployments. * **Onboarding Projects v2**: Fixed onboarding flow to create Projects v2. ## Features * **Settings Side Menu**: Added settings side menu for improved navigation. * **Configurable Decision Max Depth**: Decision max depth is now configurable. ## Fixes * **ZEN Engine and JDM Editor**: Updated to the latest versions with bug fixes and improvements. * **Release Compare Order**: Fixed reversed order in release comparison. * **Commit Changes Button**: Fixed commit changes primary button behavior. * **Security Patches**: Upgraded high vulnerability dependencies. ## Features * **AWS Lambda and S3/CloudFront hosting support**: BRMS can now be deployed as two separate packages, the API on Lambda and the UI on S3/CloudFront. Serverless hosting is available on Enterprise Serverless or Enterprise Gov plans only. * **SSO authentication via OIDC PKCE**: Sign-in is now supported through generic OIDC with PKCE flow, with token validation handled via JWKS URI. Tested with Okta, Entra ID, Keycloak, and PingID; other standards-compliant providers should also work. * **Audit log export and filtering**: Audit logs can now be exported and filtered by project or user. ## Fixes * **Projects v2 versioning**: Corrected semantic versioning behavior for Projects v2. * **Release download links**: Downloads now use internal links rather than redirects. * **Security patches**: Updated dependencies to address recent vulnerabilities. ## Features * **Enhanced Simulator for Rules IDE (Projects v2)**: A redesigned simulator that makes debugging and development easier. It now offers clear access to nested sub-decisions and iterations directly from simulator trace panel, helping teams understand how complex logic executes step by step. ## Major Features * **Rules IDE (Projects v2)**: A redesigned workspace that introduces a repository-style experience. Teams can work across multiple decision models simultaneously, prepare changes before committing, and keep work organised. Branching is now built-in, allowing users to create isolated branches from main and merge them when ready. The Rules IDE adds powerful in-browser tooling: advanced diff, model comparison, conflict resolution, and change-request flows for merging into main (available on all plans). The updated interface brings a fresh UI/UX, refined navigation, and a polished brand look. Projects v1 remain fully supported. Users can continue working with them as they are, and may create new V2 projects from existing V1 ones. No automatic upgrade is performed, keeping all current setups intact. ## Features * **Releases (Projects v2)**: Users can prepare draft releases, give them names and descriptions, and publish them when finalized. Publishing locks the release into an immutable semantic-versioned artifact. ## Fixes * Zen Engine performance improvements resulting in faster and more stable rule execution. * Decision Table UI optimization for smooth handling of large tables with many columns and rows. * General UI and codebase refinements delivering a more consistent and reliable experience. ## Fixes * **Release download / deploy hanging**: Fixed issue that could cause release deploy or release download hanging. ## Fixes * **Excel Import**: Upgraded JDM Editor that enables advanced excel importing. Header mapping allows excel imports from a non GoRules JDM format. ## Features * **Compact trace**: Added support for compact trace during simulator for huge models, context and traces. ## Fixes * **Dependency vulnerabilities**: High, medium and low vulnerability package upgrades. * **PAT not visible**: Personal access tokens not visible in the profile or Service User. ## Features * **Global Roles**: Added support for easier access management, granular permissions and project access at the global level, independent of project groups and members. ## Fixes * **Dependency vulnerabilities**: Medium and low vulnerability package upgrades. ## Fixes * **PAT (cloud only)**: Fixed cloud PAT org subdomain comparison. * **Access Control**: Fixed broken access control where users could use some of the premium features. * **CORS Configuration**: Configuration now allows for CORS to be configured on a specific domain. * **ZEN Engine update**: New datetime capabilities, improved timezone performance. * **Diff Change**: Now allowed on all plans. * **Active Simulation**: Now allowed on all plans. * **SMTP**: TLS options fixed for the custom SMTP configuration. ## Features * **Views**: Admins can now create customized views of Decision Graph to control what business users see and do. For instance, you can show only specific decision tables and expressions while restricting access to column configuration options. * **Group Permissions**: More granular permissions on Documents, new permissions: * Full (create, update, delete, move, copy, restore, update graph content) * Edit Content (update graph content) * View Content (view graph content only) * Edit View (update view content only) ## Other * **Supply Chain Attestation for BRMS and Agent**: Integrated SBOM and build provenance support to verify what's inside the image and how it was built, enhancing supply chain security. ## Features * **Offline License (Enterprise Gov)**: Ability for BRMS to work in a fully offline mode, this feature is available only on the Enterprise Gov plan. ## Fixes * **Division by 0**: Division by 0 is now safe and returns null. ## Fixes * **Date v2 Diff**: Fixes incorrect implementation of diff function. * **Rounding strategy**: Rounding 0.5 now correctly returns 1. ## Features * **Release Management Permissions Update**: Added new `releases:deploy` permission. * **Document Publishing**: Only users with `admin | documents` can both request and execute document/decision publishing (after approvals). * **Environment Deployment**: Only users with `admin | releases:deploy` can both request and execute release deployments (after approvals). * **Approval Process**: Self-approval is blocked; anyone can approve but only approvals from environment-specific approval groups count (GitHub-style system) - no bypassing. * **Change Management**: Any user can cancel pending change requests. * **Updated ZEN Engine**: Latest ZEN Engine is now available. ## Fix * **Function UI crash**: Fixed an error that caused function to crash in the UI in some circumstances. ## Features * **Updated ZEN Engine**: Latest ZEN Engine is now available, including many bug fixes and improvements. ## Other * Improved logging levels - logs for failed requests are now less noisy. * Updated dependencies. ## Features * **Tag Decision Version**: Display to which release document version belongs. * **Service Account**: Create service account for BRMS API management. ## Other * Editor and engine have been updated to the latest version, which includes features and fixes such as: * New DateTime (`d`) functions, for more details please see the [ZEN changelog](https://github.com/gorules/zen/pull/345). * Updated `round` so that it now supports number of decimals as optional second argument, added `trunc`. **Breaking Change**: Missing closing brackets are no longer allowed. Ensure all your expressions that call functions are properly formatted. For example: `contains("a", "b"` no longer works - bracket needs to be closed: `contains("a", "b")`. ## Features * **AI Agent (BETA)**: You can now use AI to generate rules (enterprise only). For more information contact us. * **Administration UI Redesign**: Revamp of administrative UI/UX. * **Azure Postgres IAM**: You can now connect to Azure Postgres using IAM. * **Decision Templates**: Explore a collection of over 80 ready-made decision templates. * **Opentelemetry**: Opentelemetry is now supported through standard OTEL environment variables (you also need to pass `OTEL_ENABLED`). ## Other * Increased limit for test events (x3). * Editor and engine have been updated to the latest version, which includes many features and fixes. ## Fix * **Graph editor white screen**: Added handling of un-serializable console.log in functions. ## Fix * **Excel Export / Import column reorder**: Fixed issue with Excel export in Decision table when columns have same column title. * **Unable to invite members to a project**: Fixed issue with user search pagination on Member Invite Dialog. * **\[Cloud BRMS] Google Auth**: Resolved Google and GitHub auth on Cloud BRMS. ## Fix * **Unable to use BRMS due to non standard IP address**: Resolved issue with AuditLogs when users have non standard IPs such as combination of multiple IPs. ## Fix * **Graph editor blank screen**: Resolved issue with blank screen appearing during graph editing. ## Features * **BRMS HTTPS support**: Organizations can enable HTTPS by configuring the `HTTP_SSL_KEY` and `HTTP_SSL_CERT` environment variables. Values are in base64 encoded format. * **Decision model Save & Publish**: Enhanced UX for Save and Publish actions. ## Fix * **Document versions**: Resolved pagination issue that prevented all document versions from displaying. * **Excel Export/Import fix**: Excel export and import will not override decision table settings such as passthrough, input field etc. * **Google Cloud Deployment**: Fixed issue with BRMS minification and Release deployment to Google Cloud Storage. ## Features * **Custom Claims**: Configure custom claim name for SSO via environment variable `SSO_OAUTH2_CUSTOM_CLAIM_NAME`. ## Features * **Change Requests**: Configure approval workflows to ensure that changes are reviewed and authorized by different groups before being published or deployed. Approval workflows can be enabled across two touchpoints: * **Decision Model Change Requests**: Manage requests to publish or unpublish decision model versions. * **Release Deployment Change Requests**: Control and track the deployment of releases to different environments. * **Decision version UI updates**: The versioning system improves organization by introducing sub-versions and allowing parent versions to be named. Versioning and sub-versioning occur automatically. ## Fix * **Incorrect type check**: Fixed issue with incorrect type check from simulator. ## Features * **Request / Response JSON Schema**: Adds support for enforcing model Request and Response payloads using JSON Schema. Available in Agent from version 1.12.0. ## Features * **Test Events**: Save and manage requests across decisions. * **Active Simulation**: Get instant feedback every time you make changes your graph while developing. * **BRMS Serverless (Alpha)**: Added initial scaffold to run BRMS on AWS Lambda. Will be available exclusively to Enterprise customers with a specialized Enterprise Agreement. ## Features * **Save Decision Table Sizes**: Decision table column sizes are now retained in the local cache. ## Features * **IAM for Deployments**: Added IAM support for Deployment Configuration from BRMS to AWS S3, Azure Storage and GC Storage. ## Fix * **Loop sub-decision**: Fixed issue with switch node in sub-decision during loop. * **Release Compare**: Fixed issue with selecting release for compare selecting multiple environments. ## Features * **Improve Graph UI/UX**: Improved graph interface for better usability. * **Partial trace**: Even if part of the graph fails, you will be able to inspect the data until the point of failure. ## Features * **Decisions Diff Viewer**: Visually preview changes before saving (Business & Enterprise). * **Release Comparison**: Compare two releases with Decision Diff Viewer per release file (Enterprise Only). * **Zen Engine & JDM Editor update**: Update to the latest package versions. ## Fix * **Node Data Merge**: Fixed issue with node merging mutation. ## Features * **Update QuickJS**: Our JavaScript Engine powering our functions has been updated. ## Fix * **Decision table Loop**: Fixes an issue with decision table loops where only first iteration properly calculated unary column. ## Fix * **Unsaved changes restoration**: Ability to restore un-saved changes in case of session expiration. ## Features * **Evaluate Releases**: Implement release evaluation endpoint (Business and Enterprise only). ## Fix * **Hash collision**: Fix hash collision which could occur in some circumstances in Evaluate Environment endpoint. * **Create Project Form**: Improve UX by auto-suggesting a project key. Improve accessibility. ## Fix * **Environment Modify / Delete**: Unable to delete or modify environments when deployment configuration is invalid. * **Graph Settings Panel**: Input and output fields not having proper default value. ## Features * **Performance improvements**: Improved performance by enabling selective rendering (only visible elements) to graph. * Updated to React 18. ## Fix * **Performance and unsaved changes fix**: Fixed issue with diff not being properly detected and added performance enhancements for large graphs. ## Features * **Passthrough Data Merge**: Introduced improved data handling, enabling nodes to merge incoming and outgoing data seamlessly. This allows more flexibility in decision modeling. * **Loops**: Added support for processing arrays by evaluating each element individually across Expression, Decision Table, and Sub-Decision nodes, enhancing automation capabilities. * **Optional Response Node**: Graphs can now return data at the point where processing stops, without requiring a response node. * **Small Editor Restyle**: Minor visual improvements for a better user experience. * **Intellisense - Advanced Inferring**: Enhanced autocomplete features with more accurate type inference. Please see the support matrix for platform-specific engines for passthrough data merge and loops. ## Fix * **Personal Access Token (PAT) - Indefinite Fix**: Resolved issues related to PAT expiration. * **Engine and Editor Version Bumps**: Updated core components for better stability. * **Intellisense - Stale Inferred Request Type**: Fixed issues with autocomplete suggestions providing outdated types. ## Features * **Loops - Sub-Decision Node**: Added loop functionality to sub-decision nodes. * **Virtual Environments**: Ability to deploy a release to a virtual environment. ## Fix * **Evaluate Environment Endpoint Access Token Fix**: Addressed issues with environment access, even with valid tokens. * **Intellisense - Sub Decision Input Types**: Improved handling of input types for sub-decision nodes. ## Features * **Core Engine Upgrades**: Various performance enhancements for smoother operation. ## Features * **Intellisense v1**: Initial release of Intellisense support for improved code suggestions. ## Fix * **Deployment Run Name Fix**: Corrected naming issues during deployment processes. ## Features * **Move Documents to Folder**: Added support for moving documents. ## Features * **Environments and External Deployments**: Enhanced deployment flexibility. # Azure DevOps Source: https://docs.gorules.io/developers/cicd/azure-devops Deploy rules from GoRules BRMS to your object storage with Azure Pipelines. When a release is deployed to a BRMS virtual (Stage) environment, a webhook queues an Azure Pipelines run. The run pulls the rules artifact from BRMS and uploads it to your object storage, where the [Agent](/developers/deployment/agent/overview) or an SDK loader reads it. The webhook is push-based: runs happen only on deploys, and the payload identifies the project and target. The template only pulls; the publish step is yours. The examples cover Amazon S3, Azure Blob Storage, and Google Cloud Storage. ## How it works ```mermaid theme={null} sequenceDiagram box transparent GoRules - cloud or self-hosted participant BRMS as GoRules BRMS end participant ADO as Azure Pipelines box transparent Your account - one per environment participant S3 as Object storage
(S3 / Blob / GCS) participant Agent as GoRules Agent end BRMS->>ADO: Release deployed - queue run with GRL_PAYLOAD Note over ADO,BRMS: gorules pull (one command) ADO->>BRMS: Resolve target via Rules Sync API ADO->>BRMS: Download release artifact BRMS-->>ADO: Rules artifact (zip) ADO->>S3: Upload artifact Agent->>S3: Load rules ``` One environment is shown; each environment has its own storage location and Agent, and the pipeline routes by the payload's target. 1. A release is deployed to a virtual (Stage) environment in BRMS, directly or through a change request. 2. The BRMS webhook queues a run through the Azure DevOps Pipelines API, passing the event as the `GRL_PAYLOAD` run variable. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts. 3. The [official steps template](https://github.com/gorules/cli/blob/main/templates/azure-pipelines-pull.yml) resolves the target and downloads the artifact. Resolution and download are two API calls under the hood, but a single `gorules pull` - the template handles both. 4. Your publish step - appended after the template in the same job - uploads the artifact wherever your runtime reads it from. Without `GRL_PAYLOAD`, project and target come from whatever you pass explicitly - queue-time values or schedule configuration. Nothing environment-specific is baked into the pipeline file. Polling is also supported: a scheduled run passes the previous release id as the `current` parameter - see [Pipeline variables](#pipeline-variables). Without `GRL_PAYLOAD`, a run must state project and target explicitly (queue-time values or schedule configuration); a missing project fails fast. ## Prerequisites * BRMS webhooks (eligible plan): an organisation admin connects the Azure DevOps integration; a Project Admin (Manage Project) creates the webhook. * A Stage [environment](/brms/deploy/environments) in the project (**Settings → Environments**): deploying a release to it fires the webhook and defines `env:`. * A secret pipeline variable `GORULES_TOKEN` (project access token with read access, **Settings → Tokens**). * A GitHub service connection to reference the template repository, plus credentials for your storage destination; the examples show only the upload step. ## 1. Create the pipeline `templates/azure-pipelines-pull.yml` in the [gorules/cli](https://github.com/gorules/cli) repository is a **steps template**: it only pulls - downloading the artifact and setting result variables - and your publish step follows it in the same job. The template steps are identical in every variant; only the publish step differs. Pick your destination - each tab is the complete file: ```yaml azure-pipelines.yml theme={null} trigger: none # runs are queued by BRMS, manually, or on a schedule resources: repositories: - repository: gorules type: github name: gorules/cli ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production endpoint: jobs: - job: deploy_rules pool: vmImage: ubuntu-latest steps: - template: templates/azure-pipelines-pull.yml@gorules parameters: url: https://acme.us1.gorules.io # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD # AUTH: the AWS service connection carries credentials - role # assumption, or OIDC with AWS Toolkit >= 1.15. No keys in the pipeline - task: AWSShellScript@1 displayName: Deploy to S3 inputs: awsCredentials: regionName: eu-central-1 scriptType: inline inlineScript: | set -euo pipefail case "$(rulesTarget)" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; # anything else - including 'main', the unreleased branch head - is # refused: this pipeline deploys releases, not working state *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;; esac aws s3 cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" "s3://$BUCKET/rules/$(rulesProject)" # Without the extension: run the same script in a plain script step and # set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION as # secret pipeline variables under env: ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=S3 PROVIDER__BUCKET=acme-rules-prod # this environment's bucket PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional # AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY - optional with IAM roles # AWS_REGION - automatic on AWS compute; set it elsewhere (SDK falls back to us-east-1) ``` ```yaml azure-pipelines.yml theme={null} trigger: none # runs are queued by BRMS, manually, or on a schedule resources: repositories: - repository: gorules type: github name: gorules/cli ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production endpoint: jobs: - job: deploy_rules pool: vmImage: ubuntu-latest steps: - template: templates/azure-pipelines-pull.yml@gorules parameters: url: https://acme.us1.gorules.io # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD # AUTH: the ARM service connection carries credentials (workload # identity federation) - task: AzureCLI@2 displayName: Deploy to Blob Storage inputs: azureSubscription: scriptType: bash scriptLocation: inlineScript inlineScript: | set -euo pipefail case "$(rulesTarget)" in env:production) ACCOUNT=acmerulesprod ;; env:dev) ACCOUNT=acmerulesdev ;; *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;; esac az storage blob upload --account-name "$ACCOUNT" \ --container-name rules --name "rules/$(rulesProject)" \ --file "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" \ --auth-mode login --overwrite ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=AzureStorage PROVIDER__ACCOUNT_NAME=acmerulesprod # Entra ID (managed identity) auth # PROVIDER__CONNECTION_STRING= # alternative to account name PROVIDER__CONTAINER=rules PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional ``` ```yaml azure-pipelines.yml theme={null} trigger: none # runs are queued by BRMS, manually, or on a schedule resources: repositories: - repository: gorules type: github name: gorules/cli ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production endpoint: jobs: - job: deploy_rules pool: vmImage: ubuntu-latest steps: - template: templates/azure-pipelines-pull.yml@gorules parameters: url: https://acme.us1.gorules.io # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD - script: | set -euo pipefail # ADD YOUR AUTH HERE - workload identity federation or a # service account key case "$(rulesTarget)" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;; esac gcloud storage cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" \ "gs://$BUCKET/rules/$(rulesProject)" displayName: Deploy to GCS ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=GCS PROVIDER__BUCKET=acme-rules-prod # this environment's bucket PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional # PROVIDER__BASE64_CONTENTS= # omit to use workload identity / ADC ``` The `case` maps the target to a destination; unmapped targets fail the step so nothing is uploaded to an unintended destination. The artifact is written as `` with no `.zip` suffix, because the Agent's storage providers use the object name verbatim as the project key. See [naming the output](/developers/cli#naming-the-output) for the other layouts, and [Agent configuration](/developers/deployment/agent/overview#environment-variables) for the full provider settings - applications that evaluate through an SDK loader read the same object and need no Agent. ## 2. Declare the queue-time variable Unlike GitLab, Azure DevOps only accepts variables at queue time when the pipeline declares them. In the pipeline's **Edit → Variables**, add a variable named `GRL_PAYLOAD` (any value) with **"Let users override this value when running this pipeline"** checked. Organizations with **Limit variables that can be set at queue time** enabled - the default on newer organizations - reject the BRMS queue request entirely if this variable is not declared. If webhook deliveries fail with a queue error, this is the first thing to check. ## 3. Connect BRMS Integrations are connected once per organisation; webhooks are configured per project. 1. **Connect the Azure DevOps integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect Azure DevOps. The webhook form also deep-links here via **Configure Integrations** if the connection is missing. 2. **Create the webhook** (Project Admin - the Manage Project permission): in the project, go to **Settings → Webhooks → Create webhook**, choose type **Azure DevOps**, then select the repository (`project/repository`), the branch, and the pipeline to queue. 3. Subscribe it to the **Release Deployed** event - then every run means an environment moved, and the payload always carries an `env:` target. 4. Optionally use the **Test Webhook** section of the form to send a sample event before relying on it. See [Webhooks](/brms/setup/webhooks) for the full reference, including delivery logs and retries. **IP-restricted organizations.** Azure DevOps enforces IP restrictions through Microsoft Entra [Conditional Access policies](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/change-application-access-policies) with location conditions, plus the organization policy "Enable IP Conditional Access policy validation". If your organization uses these, BRMS's traffic must be permitted by the allowed named locations - otherwise token sign-in or the queue request is rejected even though the same configuration works from your office network. GoRules cloud has no fixed IP addresses - traffic originates from the AWS IP ranges of us-east-1 or eu-central-1 (your region). Fixes: allow those ranges (filter [ip-ranges.json](https://ip-ranges.amazonaws.com/ip-ranges.json) by region), switch to scheduled polling, or self-host BRMS with a static egress IP. ## 4. Test the flow Deploy a release to the environment in BRMS and watch the run appear under **Pipelines**. The webhook log in BRMS (**Settings → Webhooks → logs**) shows the exact payload delivered and lets you replay a delivery, so you can iterate on the pipeline without re-deploying releases. To test without BRMS, run the pipeline manually and set the `GRL_PAYLOAD` variable at queue time: ```json theme={null} { "project": { "key": "pricing" }, "target": "env:production" } ``` ## Pipeline variables The template sets these for the steps that follow it in the job: | Variable | Description | | -------------- | ------------------------------------------------------ | | `rulesChanged` | `'true'` / `'false'` | | `rulesProject` | Project that was pulled, payload-aware | | `rulesTarget` | Target that was pulled, payload-aware | | `rulesVersion` | Release version, when the target resolved to a release | | `rulesRelease` | Release id | | `rulesSha256` | Checksum of the downloaded artifact | `rulesChanged` is always `'true'` on a BRMS-triggered run - the pipeline never passes `current`, so the CLI always downloads. It exists for scheduled runs: pass the previous release id as the `current` parameter and gate your publish step on `rulesChanged` when the environment has not moved. Template parameters: `url`, `project`, `target`, `out` (default `$(Build.ArtifactStagingDirectory)/rules`), `name`, `unpack`, `delete`, `current`, and `cliVersion`. ## Committing rules to Git The same pull can also commit the rules as plain files to your repository. Three optional settings: `unpack` extracts the archive instead of writing a zip, `name: '.'` extracts straight into the output directory, and `delete` removes files that no longer exist in BRMS so the directory mirrors the target exactly. Point the output inside the checkout and commit what changed: ```yaml theme={null} steps: - checkout: self persistCredentials: true # keep credentials for the push below - template: templates/azure-pipelines-pull.yml@gorules parameters: url: https://acme.us1.gorules.io out: $(Build.SourcesDirectory)/rules unpack: true name: '.' delete: true - script: | set -euo pipefail git config user.name "gorules-bot" git config user.email "gorules-bot@acme.example" git add rules git diff --cached --quiet && exit 0 # nothing changed git commit -m "chore(rules): $(rulesProject) $(rulesTarget) $(rulesVersion)" git push origin "HEAD:$(Build.SourceBranchName)" displayName: Commit rules ``` The build service identity needs the **Contribute** permission on the repository (Project Settings → Repositories → Security), and `persistCredentials: true` keeps the job's token available for the push. `trigger: none` already prevents the push from re-queuing the pipeline. # GitHub Actions Source: https://docs.gorules.io/developers/cicd/github-actions Deploy rules from GoRules BRMS to your object storage with GitHub Actions. When a release is deployed to a BRMS virtual (Stage) environment, a webhook dispatches a GitHub Actions workflow. The run pulls the rules artifact from BRMS and uploads it to your object storage, where the [Agent](/developers/deployment/agent/overview) or an SDK loader reads it. The webhook is push-based: runs happen only on deploys, and the payload identifies the project and target. The examples cover Amazon S3, Azure Blob Storage, and Google Cloud Storage. ## How it works ```mermaid theme={null} sequenceDiagram box transparent GoRules - cloud or self-hosted participant BRMS as GoRules BRMS end participant GHA as GitHub Actions box transparent Your account - one per environment participant S3 as Object storage
(S3 / Blob / GCS) participant Agent as GoRules Agent end BRMS->>GHA: Release deployed - workflow_dispatch with payload Note over GHA,BRMS: gorules pull (one command) GHA->>BRMS: Resolve target via Rules Sync API GHA->>BRMS: Download release artifact BRMS-->>GHA: Rules artifact (zip) GHA->>S3: Upload artifact Agent->>S3: Load rules ``` One environment is shown; each environment has its own storage location and Agent, and the pipeline routes by the payload's target. 1. A release is deployed to a virtual (Stage) environment in BRMS, directly or through a change request. 2. The BRMS webhook dispatches your workflow via `workflow_dispatch`, passing the event as the `payload` input. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts. 3. The [gorules/cli pull action](https://github.com/gorules/cli) resolves the target and downloads the artifact. Resolution and download are two API calls under the hood, but a single `gorules pull` - the action handles both. 4. Your upload step - in the same job - ships the artifact to your object storage. The Agent reads it from there. Polling is also supported for setups that cannot accept inbound triggers: a scheduled workflow passes the previous release id as the `current` input and nothing is re-downloaded when the environment has not moved - see [Action outputs](#action-outputs). For a manual run, paste a payload into the **Run workflow** form; without one, the action fails fast on the missing project. ## Prerequisites * BRMS webhooks (eligible plan): an organisation admin connects the GitHub integration; a Project Admin (Manage Project) creates the webhook. * A Stage [environment](/brms/deploy/environments) in the project (**Settings → Environments**): deploying a release to it fires the webhook and defines `env:`. * A project access token with read access (**Settings → Tokens**), stored as the `GORULES_TOKEN` repository secret. * Credentials for your storage destination; the examples show only the upload step. ## 1. Create the workflow The workflow must declare a `payload` input on `workflow_dispatch` - that is where BRMS delivers the event, and the action picks it up automatically. Every variant follows the same order: pull the release, resolve the destination into a variable, authenticate (keyless OIDC), push. Pick your destination - each tab is the complete file: ```yaml .github/workflows/deploy-rules.yml theme={null} name: Deploy rules on: workflow_dispatch: inputs: payload: description: BRMS webhook payload required: false type: string permissions: id-token: write # keyless OIDC auth to the cloud provider contents: read jobs: deploy-rules: runs-on: ubuntu-latest steps: # 1. Pull the release from BRMS # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production - uses: gorules/cli/actions/pull@main id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} # No project/target baked in: a BRMS-triggered run supplies both via # the payload; a manual run pastes a payload in the Run workflow form out: dist # 2. Resolve the destination from the pulled target - name: Pick destination id: route env: TARGET: ${{ steps.rules.outputs.target }} run: | case "$TARGET" in env:production) echo "bucket=acme-rules-prod" >> "$GITHUB_OUTPUT" ;; env:dev) echo "bucket=acme-rules-dev" >> "$GITHUB_OUTPUT" ;; # 'main' and anything unmapped is refused: deploys ship releases, # not the unreleased branch head *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;; esac # 3. ADD YOUR AUTH HERE - keyless OIDC via aws-actions/configure-aws-credentials # (role-to-assume + aws-region; uses the id-token permission above) # 4. Push - name: Deploy to S3 env: PROJECT: ${{ steps.rules.outputs.project }} run: aws s3 cp "dist/$PROJECT" "s3://${{ steps.route.outputs.bucket }}/rules/$PROJECT" ``` ```yaml .github/workflows/deploy-rules.yml theme={null} name: Deploy rules on: workflow_dispatch: inputs: payload: description: BRMS webhook payload required: false type: string permissions: id-token: write # keyless OIDC auth to the cloud provider contents: read jobs: deploy-rules: runs-on: ubuntu-latest steps: # 1. Pull the release from BRMS # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production - uses: gorules/cli/actions/pull@main id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} # No project/target baked in: a BRMS-triggered run supplies both via # the payload; a manual run pastes a payload in the Run workflow form out: dist # 2. Resolve the destination from the pulled target - name: Pick destination id: route env: TARGET: ${{ steps.rules.outputs.target }} run: | case "$TARGET" in env:production) echo "account=acmerulesprod" >> "$GITHUB_OUTPUT" ;; env:dev) echo "account=acmerulesdev" >> "$GITHUB_OUTPUT" ;; # 'main' and anything unmapped is refused: deploys ship releases, # not the unreleased branch head *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;; esac # 3. ADD YOUR AUTH HERE - keyless OIDC via azure/login # (client-id / tenant-id / subscription-id; uses the id-token permission above) # 4. Push - name: Deploy to Blob Storage env: PROJECT: ${{ steps.rules.outputs.project }} run: | az storage blob upload --account-name "${{ steps.route.outputs.account }}" \ --container-name rules --name "rules/$PROJECT" \ --file "dist/$PROJECT" --auth-mode login --overwrite ``` ```yaml .github/workflows/deploy-rules.yml theme={null} name: Deploy rules on: workflow_dispatch: inputs: payload: description: BRMS webhook payload required: false type: string permissions: id-token: write # keyless OIDC auth to the cloud provider contents: read jobs: deploy-rules: runs-on: ubuntu-latest steps: # 1. Pull the release from BRMS # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production - uses: gorules/cli/actions/pull@main id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} # No project/target baked in: a BRMS-triggered run supplies both via # the payload; a manual run pastes a payload in the Run workflow form out: dist # 2. Resolve the destination from the pulled target - name: Pick destination id: route env: TARGET: ${{ steps.rules.outputs.target }} run: | case "$TARGET" in env:production) echo "bucket=acme-rules-prod" >> "$GITHUB_OUTPUT" ;; env:dev) echo "bucket=acme-rules-dev" >> "$GITHUB_OUTPUT" ;; # 'main' and anything unmapped is refused: deploys ship releases, # not the unreleased branch head *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;; esac # 3. ADD YOUR AUTH HERE - keyless via google-github-actions/auth # (workload_identity_provider + service_account; uses the id-token permission above) # 4. Push - name: Deploy to GCS env: PROJECT: ${{ steps.rules.outputs.project }} run: gcloud storage cp "dist/$PROJECT" "gs://${{ steps.route.outputs.bucket }}/rules/$PROJECT" ``` BRMS sends the same event for every environment - `deploy to dev` and `deploy to production` differ only in the payload's `target`. The `case` maps the target to a destination; unmapped targets fail the run so nothing is uploaded to an unintended destination. The artifact is written as `dist/` with no `.zip` suffix, because the Agent's storage providers use the object name verbatim as the project key - upload it as-is and the Agent picks it up. See [naming the output](/developers/cli#naming-the-output) for the other layouts, and [Agent configuration](/developers/deployment/agent/overview#environment-variables) for the full provider settings - applications that evaluate through an SDK loader read the same object and need no Agent. ## 2. Connect BRMS Integrations are connected once per organisation; webhooks are configured per project. 1. **Connect the GitHub integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect GitHub. The webhook form also deep-links here via **Configure Integrations** if the connection is missing. 2. **Create the webhook** (Project Admin - the Manage Project permission): in the project, go to **Settings → Webhooks → Create webhook**, choose type **GitHub**, then select the repository, the branch to dispatch on (for example `main`), and the workflow file (`deploy-rules.yml`). 3. Subscribe it to the **Release Deployed** event - then every run means an environment moved, and the payload always carries an `env:` target. 4. Optionally use the **Test Webhook** section of the form to send a sample event before relying on it. See [Webhooks](/brms/setup/webhooks) for the full reference, including delivery logs and retries. **Organization IP allow list.** GitHub Enterprise Cloud organizations can restrict access with an [IP allow list](https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization) (Settings → Security → IP allow list). If enabled, the setting "Enable IP allow list configuration for installed GitHub Apps" governs whether app traffic is exempt - otherwise BRMS's traffic must be allowed. A blocked IP makes the dispatch fail as if the repository did not exist, so check this first when a correctly configured webhook cannot reach the repo. GoRules cloud has no fixed IP addresses - traffic originates from the AWS IP ranges of us-east-1 or eu-central-1 (your region). Fixes: allow those ranges (filter [ip-ranges.json](https://ip-ranges.amazonaws.com/ip-ranges.json) by region), switch to scheduled polling, or self-host BRMS with a static egress IP. ## 3. Test the flow Deploy a release to the environment in BRMS and watch the workflow run appear under the repository's **Actions** tab. The webhook log in BRMS (**Settings → Webhooks → logs**) shows the exact payload delivered and lets you replay a delivery, so you can iterate on the workflow without re-deploying releases. To test without BRMS, trigger the workflow manually from the Actions tab and paste a payload: ```json theme={null} { "project": { "key": "pricing" }, "target": "env:production" } ``` ## Action outputs | Output | Description | | --------- | ---------------------------------------------------------------------- | | `project` | Project that was pulled, payload-aware | | `target` | Target that was pulled, payload-aware | | `changed` | `false` when the target still matches the `current` input, else `true` | | `release` | Release id, when the target resolved to a release | | `version` | Release version, when the target resolved to a release | | `commit` | Commit id, when the target resolved to a commit | | `sha256` | Checksum of the downloaded artifact | | `files` | JSON array of the paths written | `changed` is always `true` on a BRMS-triggered run - the workflow never passes the `current` input, so the action always downloads, which is why the workflow above has no `if:` guards: every trigger means a fresh deploy, and re-running a delivery re-uploads the same artifact, which is idempotent. The output exists for scheduled pulls: persist the last `release` output (for example in a cache), feed it back as the `current` input, and gate the upload steps with `if: steps.rules.outputs.changed == 'true'` - the action then skips the download entirely when the environment has not moved. See the [CLI exit codes](/developers/cli#exit-codes) for the underlying contract. ## Committing rules to Git The same pull can also commit the rules as plain files to your repository. Three optional settings: `unpack` extracts the archive instead of writing a zip, `name: '.'` extracts straight into the output directory, and `delete` removes files that no longer exist in BRMS so the directory mirrors the target exactly. Point the output inside the checkout and commit what changed: ```yaml theme={null} permissions: contents: write # lets the default GITHUB_TOKEN push jobs: sync-rules: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production - uses: gorules/cli/actions/pull@main id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} out: rules unpack: true name: '.' delete: true - name: Commit rules run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add rules git diff --cached --quiet && exit 0 # nothing changed git commit -m "chore(rules): ${{ steps.rules.outputs.project }} ${{ steps.rules.outputs.target }} ${{ steps.rules.outputs.version }}" git push ``` `contents: write` lets the default `GITHUB_TOKEN` push, and since the workflow only runs on `workflow_dispatch`, the push cannot re-trigger it. # GitLab CI Source: https://docs.gorules.io/developers/cicd/gitlab-ci Deploy rules from GoRules BRMS to your object storage with GitLab CI. When a release is deployed to a BRMS virtual (Stage) environment, a webhook triggers a GitLab pipeline. The run pulls the rules artifact from BRMS and uploads it to your object storage, where the [Agent](/developers/deployment/agent/overview) or an SDK loader reads it. The webhook is push-based: runs happen only on deploys, and the payload identifies the project and target. The examples cover Amazon S3, Azure Blob Storage, and Google Cloud Storage. ## How it works ```mermaid theme={null} sequenceDiagram box transparent GoRules - cloud or self-hosted participant BRMS as GoRules BRMS end participant GL as GitLab CI box transparent Your account - one per environment participant S3 as Object storage
(S3 / Blob / GCS) participant Agent as GoRules Agent end BRMS->>GL: Release deployed - create pipeline with GRL_PAYLOAD Note over GL,BRMS: gorules pull (one command) GL->>BRMS: Resolve target via Rules Sync API GL->>BRMS: Download release artifact BRMS-->>GL: Rules artifact (zip) GL->>S3: Upload artifact Agent->>S3: Load rules ``` One environment is shown; each environment has its own storage location and Agent, and the pipeline routes by the payload's target. 1. A release is deployed to a virtual (Stage) environment in BRMS, directly or through a change request. 2. The BRMS webhook creates a pipeline through the GitLab API, passing the event as the `GRL_PAYLOAD` variable. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts. 3. The pull job from the [official template](https://github.com/gorules/cli/blob/main/templates/gitlab-ci-pull.yml) resolves the target and downloads the artifact. Resolution and download are two API calls under the hood, but a single `gorules pull` - the template handles both. 4. Your publish job ships the artifact to your object storage. The Agent reads it from there. Polling is also supported: run the pipeline on a schedule and pass the previous release id as `GORULES_CURRENT` - see [Job results](#job-results). Without `GRL_PAYLOAD`, a run must state project and target explicitly (**Run pipeline** form variables or a pasted payload); a missing project fails fast, and the deploy job refuses the silent `main` default as a backstop. ## Prerequisites * BRMS webhooks (eligible plan): an organisation admin connects the GitLab integration; a Project Admin (Manage Project) creates the webhook. * A Stage [environment](/brms/deploy/environments) in the project (**Settings → Environments**): deploying a release to it fires the webhook and defines `env:`. * Two CI/CD variables (**Settings → CI/CD → Variables**): `GORULES_URL` (org URL, e.g. `https://acme.us1.gorules.io`) and `GORULES_TOKEN` (project access token, **Masked**; **Protected** only if the pipeline runs on a protected branch). * Credentials for your storage destination; the examples show only the upload command. ## 1. Create the pipeline `templates/gitlab-ci-pull.yml` in the [gorules/cli](https://github.com/gorules/cli) repository defines a hidden job, `.gorules-pull`, that you `extends:`. The pull job is identical in every variant; only the deploy job differs. Pick your destination - each tab is the complete file: ```yaml .gitlab-ci.yml theme={null} include: # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml' workflow: rules: - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing pull:rules: extends: .gorules-pull # No project/target baked in: a BRMS-triggered run takes both from the # webhook payload, and a manual run states them explicitly in the Run # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD) deploy:rules: needs: ['pull:rules'] image: name: amazon/aws-cli:latest entrypoint: [''] resource_group: rules-deploy # serialize concurrent deploys script: # 1. resolve the destination from the pulled target - | case "$RULES_TARGET" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; # anything else - including 'main', the unreleased branch head - is # refused: this pipeline deploys releases, not working state *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;; esac # 2. ADD YOUR AUTH HERE - keyless via GitLab OIDC (id_tokens + # assume-role-with-web-identity, see https://docs.gitlab.com/ci/cloud_services/aws/), # or masked AWS_* CI/CD variables (injected automatically - nothing to add) # 3. push - aws s3 cp "dist/$RULES_PROJECT" "s3://$BUCKET/rules/$RULES_PROJECT" - echo "Deployed $RULES_PROJECT@$RULES_RELEASE to $BUCKET" ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=S3 PROVIDER__BUCKET=acme-rules-prod # this environment's bucket PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional # AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY - optional with IAM roles # AWS_REGION - automatic on AWS compute; set it elsewhere (SDK falls back to us-east-1) ``` ```yaml .gitlab-ci.yml theme={null} include: # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml' workflow: rules: - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing pull:rules: extends: .gorules-pull # No project/target baked in: a BRMS-triggered run takes both from the # webhook payload, and a manual run states them explicitly in the Run # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD) deploy:rules: needs: ['pull:rules'] image: mcr.microsoft.com/azure-cli:latest resource_group: rules-deploy script: # ADD YOUR AUTH HERE - OIDC federation or a service principal - | case "$RULES_TARGET" in env:production) ACCOUNT=acmerulesprod ;; env:dev) ACCOUNT=acmerulesdev ;; *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;; esac - az storage blob upload --account-name "$ACCOUNT" --container-name rules --name "rules/$RULES_PROJECT" --file "dist/$RULES_PROJECT" --auth-mode login --overwrite ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=AzureStorage PROVIDER__ACCOUNT_NAME=acmerulesprod # Entra ID (managed identity) auth # PROVIDER__CONNECTION_STRING= # alternative to account name PROVIDER__CONTAINER=rules PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional ``` ```yaml .gitlab-ci.yml theme={null} include: # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml' workflow: rules: - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing pull:rules: extends: .gorules-pull # No project/target baked in: a BRMS-triggered run takes both from the # webhook payload, and a manual run states them explicitly in the Run # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD) deploy:rules: needs: ['pull:rules'] image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim resource_group: rules-deploy script: # ADD YOUR AUTH HERE - workload identity federation or a service account key - | case "$RULES_TARGET" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;; esac - gcloud storage cp "dist/$RULES_PROJECT" "gs://$BUCKET/rules/$RULES_PROJECT" ``` **Agent setup** - each environment's Agent reads the same location: ```shell theme={null} PROVIDER__TYPE=GCS PROVIDER__BUCKET=acme-rules-prod # this environment's bucket PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional # PROVIDER__BASE64_CONTENTS= # omit to use workload identity / ADC ``` BRMS sends the same event for every environment - `deploy to dev` and `deploy to production` differ only in the payload's `target`, which the pull job re-exports as `RULES_TARGET`. The `case` maps the target to a destination; unmapped targets fail the job so nothing is uploaded to an unintended destination. The pull job publishes its results as a [dotenv report](#job-results), so `deploy:rules` reads `RULES_TARGET` and `RULES_PROJECT` as ordinary variables in its script - no artifact parsing. There is no changed-gate: a BRMS-triggered run never passes `GORULES_CURRENT`, so the CLI always downloads; re-running a delivery re-uploads the same artifact. `RULES_CHANGED` only matters for [scheduled pulls](#job-results). The pulled files arrive via `needs:` as a job artifact in `dist/`. The artifact is written as `dist/` with no `.zip` suffix, because the Agent's storage providers use the object name verbatim as the project key - upload it as-is and the Agent picks it up. See [naming the output](/developers/cli#naming-the-output) for the other layouts, and [Agent configuration](/developers/deployment/agent/overview#environment-variables) for the full provider settings - applications that evaluate through an SDK loader read the same object and need no Agent. ## 2. Connect BRMS Integrations are connected once per organisation; webhooks are configured per project. 1. **Connect the GitLab integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect GitLab. The webhook form also deep-links here via **Configure Integrations** if the connection is missing. 2. **Create the webhook** (Project Admin - the Manage Project permission): in the project, go to **Settings → Webhooks → Create webhook**, choose type **GitLab**, then select the repository and the branch to run on (for example `main`). 3. Subscribe it to the **Release Deployed** event - then every run means an environment moved, and the payload always carries an `env:` target. 4. Optionally use the **Test Webhook** section of the form to send a sample event before relying on it. See [Webhooks](/brms/setup/webhooks) for the full reference, including delivery logs and retries. BRMS creates the pipeline through the authenticated GitLab API, so no pipeline trigger token is needed and no queue-time variable has to be declared - `GRL_PAYLOAD` just arrives. **`404 Project Not Found` on delivery.** GitLab hides anything the caller cannot access behind a 404, so this one error has four possible causes: 1. **IP restrictions.** If the GitLab group restricts access by IP, it blocks BRMS. GoRules cloud has no fixed IP addresses - traffic originates from the AWS IP ranges of us-east-1 or eu-central-1 (your region). Fixes: allow those ranges (filter [ip-ranges.json](https://ip-ranges.amazonaws.com/ip-ranges.json) by region), switch to scheduled polling, or self-host BRMS with a static egress IP. 2. **Repository role.** The identity connected in the integration needs Developer or higher on the repository. 3. **CI/CD disabled.** The project's CI/CD feature must be enabled (project Settings → General → Visibility). 4. **Stale repository selection.** Re-select the repository in the webhook after reconnecting the integration. The delivery log (**Settings → Webhooks → View Logs**) shows each request and response; **Retry** re-sends it. ## 3. Test the flow Deploy a release to the environment in BRMS and watch the pipeline appear under **Build → Pipelines**. The webhook log in BRMS (**Settings → Webhooks → logs**) shows the exact payload delivered and lets you replay a delivery, so you can iterate on the pipeline without re-deploying releases. To test without BRMS, use **Build → Pipelines → New pipeline** and either set `GORULES_PROJECT` and `GORULES_TARGET` directly, or add a `GRL_PAYLOAD` variable to exercise the exact webhook path: ```json theme={null} { "project": { "key": "pricing" }, "target": "env:production" } ``` ## Job results The pull job writes a dotenv report, so any job with `needs: ['pull:rules']` reads these as ordinary variables in `script:` and `environment:` (not in `rules:`, which GitLab evaluates before the pipeline runs): | Variable | Description | | --------------- | ------------------------------------------------------ | | `RULES_CHANGED` | `'true'` / `'false'` | | `RULES_PROJECT` | Project that was pulled, payload-aware | | `RULES_TARGET` | Target that was pulled, payload-aware | | `RULES_VERSION` | Release version, when the target resolved to a release | | `RULES_RELEASE` | Release id | | `RULES_SHA256` | Checksum of the downloaded artifact | Job configuration (set under the extending job's `variables:`): `GORULES_PROJECT`, `GORULES_TARGET`, `GORULES_OUT` (default `dist`), `GORULES_NAME`, `GORULES_UNPACK` and `GORULES_DELETE` (quote the booleans: `'true'`), `GORULES_CURRENT`, and `GORULES_CLI_VERSION`. `RULES_CHANGED` is always `'true'` on a BRMS-triggered run - the pipeline never passes `GORULES_CURRENT`, so the CLI always downloads, which is why the deploy job above has no gate. The flag exists for scheduled pulls: run the pipeline on a schedule, pass the last `RULES_RELEASE` back as `GORULES_CURRENT`, and when the environment has not moved the CLI exits with code `3`, the job reports `RULES_CHANGED=false`, and downstream jobs skip their upload with a script guard: ```yaml theme={null} script: - if [ "$RULES_CHANGED" != "true" ]; then echo "Unchanged, skipping."; exit 0; fi ``` The guard must live in `script:`, not in `rules:` - GitLab evaluates job `rules:` at pipeline creation, before any job has run, so dotenv variables from an earlier job are always unset there and a `rules: - if: $RULES_CHANGED == "true"` gate would silently never match. See the [CLI exit codes](/developers/cli#exit-codes) for the underlying contract. ## Committing rules to Git The same pull can also commit the rules as plain files to your repository. Three optional settings: `unpack` extracts the archive instead of writing a zip, `name: '.'` extracts straight into the output directory, and `delete` removes files that no longer exist in BRMS so the directory mirrors the target exactly. Point the output inside the checkout and commit what changed: ```yaml theme={null} pull:rules: extends: .gorules-pull variables: GORULES_OUT: rules GORULES_UNPACK: 'true' GORULES_NAME: '.' GORULES_DELETE: 'true' commit:rules: needs: ['pull:rules'] image: alpine/git:latest script: - git config user.name "gorules-bot" - git config user.email "gorules-bot@acme.example" - git add rules - git diff --cached --quiet && exit 0 # nothing changed - git commit -m "chore(rules): $RULES_PROJECT $RULES_TARGET ${RULES_VERSION:-$RULES_RELEASE}" - git push "https://gitlab-ci-token:${GIT_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "HEAD:$CI_COMMIT_REF_NAME" ``` `GIT_PUSH_TOKEN` is a GitLab project access token with `write_repository` scope (the default CI job token cannot push). The extracted files travel from `pull:rules` to `commit:rules` as a job artifact, and the `workflow: rules:` gate from the main example already prevents the push from re-triggering the pipeline (`push` is not an allowed source). # GoRules CLI Source: https://docs.gorules.io/developers/cli Bridge your GoRules BRMS projects to local development, AI tooling, and CI/CD pipelines. The GoRules CLI (`@gorules/cli`) is an open-source command-line tool for [GoRules BRMS](https://gorules.io). It exposes an MCP (Model Context Protocol) server so AI-powered editors like Claude Code, Cursor, and Windsurf can read, evaluate, and interact with your decision logic directly, and it ships rules from BRMS into your own infrastructure with [`gorules pull`](#pulling-rules-into-a-pipeline). **npm:** [@gorules/cli](https://www.npmjs.com/package/@gorules/cli) ## Installation ```bash theme={null} # npm npm install -g @gorules/cli # Or run directly with npx npx @gorules/cli mcp start ``` ## Quick start Start the MCP bridge: ```bash theme={null} gorules mcp start ``` This launches a local server on port 41919 that: 1. Exposes an MCP endpoint for AI tools to discover and call GoRules tools 2. Exposes REST endpoints for evaluating decisions and fetching decision files 3. Opens a WebSocket connection that the GoRules browser editor connects to Once running, open your GoRules project in the browser and click **Connect MCP** to link the editor to your local CLI. ## Commands ### `gorules mcp start` Start the MCP bridge server. | Flag | Alias | Default | Description | | -------- | ----- | ----------- | --------------------- | | `--port` | `-p` | `41919` | Server port | | `--host` | `-h` | `localhost` | Server host | | `--url` | `-u` | - | GoRules server URL | | `--open` | - | `false` | Open browser on start | ### `gorules pull` Download the rules artifact for a project target. See [Pulling rules into a pipeline](#pulling-rules-into-a-pipeline). | Flag | Alias | Env | Description | | ----------- | ----- | ----------------- | -------------------------------------------------------------------------------------------------------- | | `--project` | `-p` | `GORULES_PROJECT` | Project key or ID | | `--target` | `-t` | `GORULES_TARGET` | Target to resolve (default `main`) | | `--out` | `-o` | - | Output directory (default `.`) | | `--unpack` | - | - | Extract the archive instead of writing the zip | | `--delete` | - | - | With `--unpack`: delete files not in the artifact so the directory mirrors the target exactly | | `--name` | - | - | Output file name (zip) or sub-directory name (`--unpack`); defaults to the project key with no extension | | `--current` | - | - | Release or commit ID you already hold; exits `3` when unchanged | | `--url` | `-u` | `GORULES_URL` | BRMS URL | | `--token` | - | `GORULES_TOKEN` | Project access token | | `--json` | - | - | Print the result as JSON on stdout | ## Pulling rules into a pipeline `gorules pull` resolves a target in BRMS through the Rules Sync API and downloads the matching rules artifact. It is the building block for shipping rules from BRMS into your own infrastructure: a CI job pulls the artifact and uploads it wherever your runtime - typically the [Agent](/developers/deployment/agent/overview) - reads it from. For end-to-end pipelines - BRMS webhook to object storage, with environment routing - see the CI/CD integration guides for [GitHub Actions](/developers/cicd/github-actions), [GitLab CI](/developers/cicd/gitlab-ci), and [Azure DevOps](/developers/cicd/azure-devops). ```bash theme={null} export GORULES_URL=https://acme.us1.gorules.io export GORULES_TOKEN=... # project access token, read scope is enough gorules pull --project pricing --target env:production --out ./dist aws s3 cp ./dist/ s3://my-bucket/rules/live/ --recursive ``` Run it in a pipeline with npx: ```bash theme={null} npx @gorules/cli@latest pull --project pricing --target env:production ``` For reproducible production pipelines, pin a released version (`@gorules/cli@`, action/template tag `cli-v`) instead of tracking latest. ### Targets | Target | Resolves to | | ------------------- | ------------------------------------------------- | | `main` (default) | Latest commit on the default branch | | `branch:` | Latest commit on that branch | | `commit:` | That exact commit, pinned | | `release:` | That release, by semantic version or ID | | `env:` | Whichever release is deployed to that environment | ### Naming the output By default the artifact is written as `` with **no** `.zip` suffix, because the Agent's S3, GCS, and Azure Blob providers use the object name verbatim as the project key. The Agent's local `zip` provider is the opposite - it reads `/.zip` and strips the suffix itself - so that destination needs `--name pricing.zip`. With `--unpack`, `--name` is the sub-directory to extract into (default: the project key, the layout the Agent's `filesystem` provider expects). Pass `--name .` to extract straight into `--out`, which is what you want when baking rules into a container image: ```bash theme={null} gorules pull --project pricing --target release:1.4.2 --out ./rules --unpack --name . ``` Extraction behaves like `aws s3 sync`: byte-identical files are left untouched, changed files are written atomically, and files the artifact does not carry are preserved. Add `--delete` for `s3 sync --delete` semantics, so rules deleted in BRMS are deleted on disk too. As a guard against wiping a directory it does not own, `--delete` refuses a non-empty destination without a `.config/project.json` from a previous pull. ### Exit codes Pipelines branch on the exit code: | Code | Meaning | | ---- | ---------------------------------------------------- | | `0` | Artifact downloaded | | `1` | Error | | `2` | Usage error (missing or invalid arguments) | | `3` | Nothing to do (`--current` matched what is deployed) | | `4` | No release is deployed to the target | ### GitHub Actions A composite action lives in the [gorules/cli](https://github.com/gorules/cli) repository, so the tag you pin is the CLI version you get. A `payload` sent by a BRMS [webhook](/brms/setup/webhooks) via `workflow_dispatch` is picked up automatically. ```yaml theme={null} jobs: rules: runs-on: ubuntu-latest steps: # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production - uses: gorules/cli/actions/pull@main id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} # no project/target: a BRMS-triggered run supplies both via the payload out: ./dist - name: Deploy to S3 env: PROJECT: ${{ steps.rules.outputs.project }} TARGET: ${{ steps.rules.outputs.target }} run: | case "$TARGET" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;; esac aws s3 cp "./dist/$PROJECT" "s3://$BUCKET/rules/$PROJECT" ``` The action outputs `project`, `target` (payload-aware), `changed`, `release`, `version`, `commit`, `sha256`, and `files`, so the workflow routes on what was actually pulled and a scheduled workflow can skip the upload when nothing moved. ### GitLab CI `templates/gitlab-ci-pull.yml` defines a hidden job you extend. `GORULES_URL` and `GORULES_TOKEN` are CI/CD variables; mask and protect the token. ```yaml theme={null} include: # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml' pull:rules: extends: .gorules-pull # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD publish:rules: needs: ['pull:rules'] script: # rules: cannot see dotenv variables (evaluated before jobs run) - guard in script - if [ "$RULES_CHANGED" != "true" ]; then exit 0; fi - | case "$RULES_TARGET" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; *) echo "No destination for '$RULES_TARGET'"; exit 1 ;; esac - aws s3 cp "dist/$RULES_PROJECT" "s3://$BUCKET/rules/$RULES_PROJECT" ``` The job publishes `RULES_CHANGED`, `RULES_PROJECT`, `RULES_TARGET`, `RULES_VERSION`, `RULES_RELEASE`, and `RULES_SHA256` as a dotenv report, so later jobs read them as ordinary variables. ### Azure Pipelines `templates/azure-pipelines-pull.yml` is a steps template: it pulls the artifact and sets result variables, and you append your own publish step in the same job. `GORULES_TOKEN` must exist as a secret pipeline variable or in a linked variable group. ```yaml theme={null} resources: repositories: - repository: gorules type: github name: gorules/cli ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production endpoint: jobs: - job: deploy_rules pool: vmImage: ubuntu-latest steps: - template: templates/azure-pipelines-pull.yml@gorules parameters: url: https://acme.us1.gorules.io # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD - script: | case "$(rulesTarget)" in env:production) BUCKET=acme-rules-prod ;; env:dev) BUCKET=acme-rules-dev ;; *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;; esac aws s3 cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" "s3://$BUCKET/rules/$(rulesProject)" displayName: Deploy to S3 ``` The template sets `rulesChanged`, `rulesProject`, `rulesTarget` (payload-aware), `rulesVersion`, `rulesRelease`, and `rulesSha256` for the steps that follow. For BRMS-triggered runs, the pipeline must accept the `GRL_PAYLOAD` variable at queue time: add a pipeline variable named `GRL_PAYLOAD` with "Let users override this value when running this pipeline" checked. Organizations with "Limit variables that can be set at queue time" enabled reject the queue request otherwise. ## REST API When the bridge is running, it exposes HTTP endpoints for local development. ### Evaluate a decision ```bash theme={null} POST http://localhost:41919/evaluate/{filePath} ``` ```json theme={null} { "context": { "customer": { "tier": "premium" }, "orderTotal": 150 }, "trace": false } ``` Response: ```json theme={null} { "result": { "discount": 0.15, "freeShipping": true } } ``` Optional body fields: `trace` (boolean), `maxDepth` (number). ### Retrieve a decision file ```bash theme={null} GET http://localhost:41919/file/{filePath} ``` Returns the raw decision graph JSON. You can use this as a loader for the ZEN Engine: ```javascript theme={null} import { ZenEngine } from "@gorules/zen-engine"; const engine = new ZenEngine({ loader: async (key) => { const res = await fetch(`http://localhost:41919/file/${key}`); return res.json(); }, }); const result = await engine.evaluate("my-decision", { customer: { tier: "premium" }, orderTotal: 150, }); ``` ## How it works The CLI acts as a bridge between AI tools and the GoRules browser editor: ```mermaid theme={null} graph TD A[AI Tool - Claude, Cursor, etc.] -->|MCP Protocol| B[GoRules CLI - localhost:41919] B -->|WebSocket| C[GoRules Browser Editor] C -->|API| D[GoRules BRMS Platform] ``` 1. The CLI starts and generates a connection token 2. The GoRules browser editor connects via WebSocket using the token 3. The editor sends a tool manifest - the list of available tools for that project 4. AI tools discover these tools via MCP and invoke them 5. The CLI forwards tool calls to the browser, which executes them and returns results The CLI is stateless - it doesn't store credentials or project data. All tool execution happens in the browser editor, which has an authenticated session with the GoRules platform. ## License MIT # Agent deployment Source: https://docs.gorules.io/developers/deployment/agent/deployment Deploy the GoRules Agent with Docker, Docker Compose, or Kubernetes. ## Quick start with Docker ```bash theme={null} docker run -p 8080:8080 \ -e PROVIDER__TYPE=Filesystem \ -e PROVIDER__ROOT_DIR=/data \ -v ./rules:/data \ gorules/agent:latest ``` ## Docker Compose ```yaml theme={null} version: '3.8' services: gorules-agent: image: gorules/agent:latest ports: - "8080:8080" environment: PROVIDER__TYPE: S3 PROVIDER__BUCKET: my-rules-bucket PROVIDER__PREFIX: production/ POLL_INTERVAL: 10000 # For AWS credentials, use IAM roles or mount credentials # volumes: # - ~/.aws:/root/.aws:ro ``` ### With local filesystem ```yaml theme={null} version: '3.8' services: gorules-agent: image: gorules/agent:latest ports: - "8080:8080" environment: PROVIDER__TYPE: Filesystem PROVIDER__ROOT_DIR: /data volumes: - ./rules:/data:ro ``` ### With MinIO ```yaml theme={null} version: '3.8' services: gorules-agent: image: gorules/agent:latest ports: - "8080:8080" environment: PROVIDER__TYPE: S3 PROVIDER__BUCKET: rules PROVIDER__FORCE_PATH_STYLE: "true" PROVIDER__ENDPOINT: http://minio:9000 AWS_ACCESS_KEY_ID: minioadmin AWS_SECRET_ACCESS_KEY: minioadmin depends_on: - minio minio: image: minio/minio:latest command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - minio-data:/data volumes: minio-data: ``` ## Kubernetes ### Basic deployment ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: gorules-agent spec: replicas: 3 selector: matchLabels: app: gorules-agent template: metadata: labels: app: gorules-agent spec: containers: - name: agent image: gorules/agent:latest ports: - containerPort: 8080 env: - name: PROVIDER__TYPE value: S3 - name: PROVIDER__BUCKET value: my-rules-bucket - name: PROVIDER__PREFIX value: production/ - name: POLL_INTERVAL value: "10000" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "1000m" livenessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: gorules-agent spec: selector: app: gorules-agent ports: - port: 80 targetPort: 8080 ``` ### With ConfigMap for environment ```yaml theme={null} apiVersion: v1 kind: ConfigMap metadata: name: gorules-agent-config data: PROVIDER__TYPE: "S3" PROVIDER__BUCKET: "my-rules-bucket" PROVIDER__PREFIX: "production/" POLL_INTERVAL: "10000" OTEL_ENABLED: "true" OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317" --- apiVersion: apps/v1 kind: Deployment metadata: name: gorules-agent spec: replicas: 3 selector: matchLabels: app: gorules-agent template: metadata: labels: app: gorules-agent spec: serviceAccountName: gorules-agent containers: - name: agent image: gorules/agent:latest ports: - containerPort: 8080 envFrom: - configMapRef: name: gorules-agent-config resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "1000m" livenessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 ``` ### With AWS IAM Roles for Service Accounts (IRSA) ```yaml theme={null} apiVersion: v1 kind: ServiceAccount metadata: name: gorules-agent annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/gorules-agent-role --- apiVersion: apps/v1 kind: Deployment metadata: name: gorules-agent spec: replicas: 3 selector: matchLabels: app: gorules-agent template: metadata: labels: app: gorules-agent spec: serviceAccountName: gorules-agent containers: - name: agent image: gorules/agent:latest ports: - containerPort: 8080 env: - name: PROVIDER__TYPE value: S3 - name: PROVIDER__BUCKET value: my-rules-bucket livenessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 8080 initialDelaySeconds: 3 periodSeconds: 10 ``` ### Horizontal Pod Autoscaler ```yaml theme={null} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: gorules-agent spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: gorules-agent minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 ``` ## Platform guides For detailed platform-specific instructions, see: * [Docker Compose](/developers/platform-guides/docker-compose) * [Kubernetes](/developers/platform-guides/kubernetes) * [AWS ECS](/developers/platform-guides/aws-ecs) * [Azure Container Apps](/developers/platform-guides/azure-container-apps) # Agent overview Source: https://docs.gorules.io/developers/deployment/agent/overview High-performance REST API for rule evaluation with hot reloading. The GoRules Agent is a standalone Rust microservice that evaluates decisions via REST API. It pulls releases from object storage, automatically reloads when changes occur, and requires no UI. Available on Docker Hub: `gorules/agent` ## When to use the Agent **Choose Agent when:** * Multiple services need to evaluate the same rules * You want to update rules without redeploying applications * You need REST API access to rule evaluation * You want centralized metrics and observability **Consider alternatives when:** * You need sub-millisecond latency (use [embedded SDK](/developers/deployment/embedded)) * You're in a single-service architecture (embedded may be simpler) ## Architecture ```mermaid theme={null} flowchart TB svcA["Service A"] --> agent svcB["Service B"] --> agent svcC["Service C"] --> agent agent["GoRules Agent
(REST API)"] storage["Rules Storage
(S3, GCS, Azure, Local)"] agent <--> storage ``` ## Custom Docker image Build a self-contained image with your rules baked in. This is useful for static rules that don't change frequently, or for deployments where you want versioned rule images. ```dockerfile theme={null} FROM gorules/agent:latest COPY ./rules /data ENV PROVIDER__TYPE=Filesystem ENV PROVIDER__ROOT_DIR=/data EXPOSE 8080 CMD ["./app"] ``` Build and run: ```bash theme={null} docker build -t my-rules-api . docker run -p 8080:8080 my-rules-api ``` This creates a portable REST API with your rules embedded. Deploy it anywhere containers run. ## Environment variables ### AWS S3 If your deployment supports IAM roles, credentials are optional. ```shell theme={null} PROVIDER__TYPE=S3 PROVIDER__BUCKET=my-rules-bucket PROVIDER__PREFIX=staging # Optional bucket prefix AWS_ACCESS_KEY_ID= # Optional with IAM AWS_SECRET_ACCESS_KEY= # Optional with IAM AWS_REGION=eu-central-1 # Automatic on AWS compute; set elsewhere (SDK falls back to us-east-1) ``` ### Azure Blob Storage ```shell theme={null} PROVIDER__TYPE=AzureStorage PROVIDER__ACCOUNT_NAME= # Entra ID (managed identity) auth PROVIDER__CONNECTION_STRING= # Alternative to account name PROVIDER__CONTAINER= PROVIDER__PREFIX=staging # Optional storage prefix ``` ### Google Cloud Storage ```shell theme={null} PROVIDER__TYPE=GCS PROVIDER__BUCKET= PROVIDER__BASE64_CONTENTS= # Optional with workload identity PROVIDER__PREFIX=staging # Optional bucket prefix ``` ### MinIO and S3-compatible storage ```shell theme={null} PROVIDER__TYPE=S3 PROVIDER__BUCKET=my-bucket PROVIDER__FORCE_PATH_STYLE=true PROVIDER__ENDPOINT=http://localhost:9000 PROVIDER__PREFIX=folder/ # Optional AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= ``` ### Local filesystem ```shell theme={null} PROVIDER__TYPE=Filesystem PROVIDER__ROOT_DIR=./data # Default: data ``` ### Zip file ```shell theme={null} PROVIDER__TYPE=Zip PROVIDER__ROOT_DIR=./data # Default: data ``` ### Additional options ```shell theme={null} POLL_INTERVAL=5000 # Polling interval in ms (min: 1000, default: 5000) CORS_PERMISSIVE=true # Enable permissive CORS (default: false) RELEASE_ZIP_PASSWORD= # Optional password for encrypted releases ``` ### HTTPS configuration ```shell theme={null} HTTP_SSL__CERT= HTTP_SSL__KEY= ``` ## Health probes **Path:** `/api/health` **Port:** `8080` Recommended probe configuration: | Setting | Value | | ------------- | ---------- | | Initial delay | 3 seconds | | Period | 10 seconds | ## Hot reloading The Agent automatically detects rule changes and reloads without downtime: 1. Agent polls storage at `POLL_INTERVAL` (default: 5000ms) 2. Detects new or modified rule files 3. Loads new version in background 4. Atomically swaps to new version 5. Requests in-flight complete with old version No requests are dropped during reload. ## Observability ### OpenTelemetry Enable OpenTelemetry with `OTEL_ENABLED=true`. When enabled, standard OTEL environment variables are supported: ```shell theme={null} OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 OTEL_SERVICE_NAME=gorules-agent OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production ``` See the [OpenTelemetry documentation](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for all available options. ### Logging Structured JSON logs: ```json theme={null} { "timestamp": "2025-01-15T10:30:00Z", "level": "info", "message": "Decision evaluated", "decision": "pricing", "duration_ms": 0.23 } ``` ## API reference See the [API reference](/api-reference/introduction) for endpoint documentation. # AI setup Source: https://docs.gorules.io/developers/deployment/brms/ai-setup Configure LLM providers for GoRules AI assistant. GoRules AI requires an LLM provider to be configured on your BRMS instance. Your administrator sets this up via environment variables on the server. ## Supported LLM providers | Provider | `LLM_PROVIDER` value | Supported models | | ------------------ | -------------------- | ----------------------------- | | OpenAI | `openai` | OpenAI models | | Anthropic (Claude) | `anthropic` | Anthropic models | | Google (Gemini) | `google` | Gemini models | | Amazon Bedrock | `amazon-bedrock` | Anthropic models | | Google Vertex AI | `google-vertex` | Gemini models | | Azure OpenAI | `azure-openai` | OpenAI models | | Ollama | `ollama` | Local models served by Ollama | Vertex AI and Azure currently support only their native model families. If you need cross-provider model support (e.g., Anthropic models on Vertex AI or Azure), please contact us - we are happy to add it based on customer demand. ## Environment variables | Variable | Description | Default | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `LLM_PROVIDER` | LLM provider to use | Required | | `LLM_MODEL` | Model name (e.g., `gpt-5.4`, `claude-sonnet-4-6`, `claude-opus-4-6`, `gemini-3.1-pro-preview`, `eu.anthropic.claude-opus-4-6-v1`) | Required | | `LLM_MODEL_LOWER` | Cheaper model from the same provider, used for lightweight one-shot tasks such as chat compaction and quick text generation | Falls back to `LLM_MODEL` | | `LLM_API_KEY` | API key for the provider (not required for Amazon Bedrock, Vertex, and Ollama providers) | Required | | `LLM_BASE_URL` | Custom base URL for OpenAI-compatible endpoints | - | | `LLM_TEMPERATURE` | Sampling temperature (applies to Gemini/Google providers only) | `0.4` | | `LLM_CONTEXT_WINDOW` | Context window size in tokens | Provider default | | `LLM_MAX_OUTPUT_TOKENS` | Maximum tokens per response | `32000` | | `LLM_THINKING_LEVEL` | Extended thinking level: `high`, `medium`, or `low` | `medium` | | `LLM_AZURE_RESOURCE_NAME` | Azure OpenAI resource name (required for `azure-openai`) | - | | `LLM_GCP_PROJECT` | GCP project ID (required for `google-vertex`) | - | | `LLM_GCP_LOCATION` | GCP region for Vertex AI (required for `google-vertex`, e.g. `global`) | - | ## Prompt caching GoRules AI uses prompt caching to reduce token usage and improve response times. Caching behavior depends on the provider: | Provider | Caching | | ---------------------------------- | ---------------------------- | | Anthropic (direct) | `cacheControl: ephemeral` | | Amazon Bedrock (Anthropic models) | `cachePoint` on messages | | OpenAI | Automatic (prefix caching) | | Azure OpenAI | Automatic (prefix caching) | | Gemini/Google (direct & Vertex AI) | Automatic (implicit caching) | No additional configuration is required - caching is handled automatically for all supported providers. Prompt caching can reduce token costs by **up to 90%** in some cases, though actual savings depend on the provider, model, and usage patterns. For self-hosted deployments, ensure your load balancer has response buffering disabled or streaming enabled for optimal AI assistant experience. ## Next steps Once configured, the AI assistant is available to all users on a plan with AI enabled. See [AI assistant](/brms/build/ai) for usage details. # BRMS deployment Source: https://docs.gorules.io/developers/deployment/brms/deployment Deploy the GoRules BRMS with Docker, Docker Compose, or Kubernetes. ## Docker Run BRMS with an external PostgreSQL database: ```bash theme={null} docker run -p 8080:80 \ -e DB_HOST=your-database-host \ -e DB_USER=gorules \ -e DB_PASSWORD=your-password \ -e DB_NAME=gorules \ -e LICENSE_KEY=your-license-key \ gorules/brms ``` ## Docker Compose ### Development setup Complete setup with PostgreSQL included: ```yaml theme={null} version: '3.8' services: brms: image: gorules/brms ports: - "8080:80" environment: DB_HOST: postgres DB_PORT: 5432 DB_USER: gorules DB_PASSWORD: gorules DB_NAME: gorules DB_SSL_DISABLED: "true" LICENSE_KEY: your-license-key depends_on: postgres: condition: service_healthy postgres: image: postgres:15 environment: POSTGRES_USER: gorules POSTGRES_PASSWORD: gorules POSTGRES_DB: gorules volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gorules"] interval: 5s timeout: 5s retries: 5 volumes: postgres-data: ``` ### Production setup With SSL, custom SMTP, and external database: ```yaml theme={null} version: '3.8' services: brms: image: gorules/brms ports: - "443:80" environment: # Database DB_HOST: db.example.com DB_PORT: 5432 DB_USER: gorules DB_PASSWORD: ${DB_PASSWORD} DB_NAME: gorules DB_SSL_CA: ${DB_SSL_CA} # Application APP_URL: https://rules.example.com LICENSE_KEY: ${LICENSE_KEY} # Security COOKIE_SECRET: ${COOKIE_SECRET} SESSION_DURATION_MINUTES: 480 # Email EMAIL_HOST: smtp.sendgrid.net EMAIL_PORT: 587 EMAIL_AUTH_USER: apikey EMAIL_AUTH_PASS: ${SENDGRID_API_KEY} EMAIL_FROM: noreply@example.com restart: unless-stopped ``` ## Kubernetes ### Helm chart The recommended way to deploy on Kubernetes: ```bash theme={null} # Add the Helm repository helm repo add gorules https://charts.gorules.io # Install with custom values helm install brms gorules/gorules-brms -f values.yaml ``` Download the default values file from [ArtifactHub](https://artifacthub.io/packages/helm/gorules/gorules-brms?modal=values). ### Basic deployment ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: gorules-brms spec: replicas: 1 selector: matchLabels: app: gorules-brms template: metadata: labels: app: gorules-brms spec: containers: - name: brms image: gorules/brms:latest ports: - containerPort: 80 env: - name: DB_HOST value: postgres.database.svc.cluster.local - name: DB_USER valueFrom: secretKeyRef: name: brms-db-credentials key: username - name: DB_PASSWORD valueFrom: secretKeyRef: name: brms-db-credentials key: password - name: DB_NAME value: gorules - name: LICENSE_KEY valueFrom: secretKeyRef: name: brms-license key: license-key - name: APP_URL value: https://rules.example.com resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1000m" livenessProbe: httpGet: path: /api/health port: 80 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 80 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: gorules-brms spec: selector: app: gorules-brms ports: - port: 80 targetPort: 80 ``` ### With ConfigMap and Secrets ```yaml theme={null} apiVersion: v1 kind: Secret metadata: name: brms-secrets type: Opaque stringData: db-password: your-db-password license-key: your-license-key cookie-secret: your-cookie-secret --- apiVersion: v1 kind: ConfigMap metadata: name: brms-config data: DB_HOST: "postgres.database.svc.cluster.local" DB_PORT: "5432" DB_USER: "gorules" DB_NAME: "gorules" APP_URL: "https://rules.example.com" SESSION_DURATION_MINUTES: "480" --- apiVersion: apps/v1 kind: Deployment metadata: name: gorules-brms spec: replicas: 1 selector: matchLabels: app: gorules-brms template: metadata: labels: app: gorules-brms spec: containers: - name: brms image: gorules/brms:latest ports: - containerPort: 80 envFrom: - configMapRef: name: brms-config env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: brms-secrets key: db-password - name: LICENSE_KEY valueFrom: secretKeyRef: name: brms-secrets key: license-key - name: COOKIE_SECRET valueFrom: secretKeyRef: name: brms-secrets key: cookie-secret livenessProbe: httpGet: path: /api/health port: 80 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /api/health port: 80 initialDelaySeconds: 5 periodSeconds: 5 ``` ### Ingress ```yaml theme={null} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gorules-brms annotations: kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: - rules.example.com secretName: brms-tls rules: - host: rules.example.com http: paths: - path: / pathType: Prefix backend: service: name: gorules-brms port: number: 80 ``` ### Multi-architecture clusters If running mixed architecture node pools, force Linux x86\_64: ```yaml theme={null} spec: template: spec: nodeSelector: kubernetes.io/arch: amd64 kubernetes.io/os: linux ``` ## Serverless deployment Serverless deployment options (AWS Lambda, Azure Functions, Google Cloud Run) are available as part of the Enterprise plan. Get in touch to discuss serverless deployment options ## Platform guides For detailed platform-specific instructions, see: * [Docker Compose](/developers/platform-guides/docker-compose) * [Kubernetes](/developers/platform-guides/kubernetes) * [AWS ECS](/developers/platform-guides/aws-ecs) * [Azure Container Apps](/developers/platform-guides/azure-container-apps) # Git integrations Source: https://docs.gorules.io/developers/deployment/brms/integrations Configure GitHub, GitLab and Azure DevOps integrations for Git Sync and webhooks in self-hosted deployments. Git Sync and Git-based [webhooks](/brms/setup/webhooks) require integration with a Git provider (GitHub, GitLab or Azure DevOps). This guide covers the server-side configuration for self-hosted BRMS deployments. For instructions on connecting and using Git Sync from the BRMS interface, see [Git Sync](/brms/setup/git-sync). ## Prerequisites Before configuring Git integrations, ensure you have: 1. A running BRMS instance with a publicly accessible URL (required for OAuth callbacks) 2. [Secrets management](/developers/deployment/brms/secrets-management) configured (required for GitLab and Azure DevOps) ## Required environment variables All provider integrations require these environment variables. Set them before proceeding with provider-specific configuration. ```bash theme={null} # Your BRMS instance URL (used for OAuth callbacks) APP_URL=https://brms.yourcompany.com # Secret for signing OAuth state tokens (min 32 characters) # Generate with: openssl rand -hex 32 APP_INTEGRATIONS_SECRET=your-random-secret-minimum-32-characters ``` | Variable | Description | | ------------------------- | --------------------------------------------------------- | | `APP_URL` | Your BRMS instance URL | | `APP_INTEGRATIONS_SECRET` | Secret for signing OAuth state tokens (min 32 characters) | ## GitHub configuration GitHub integration uses a GitHub App for authentication and repository access. ### Step 1: Create a GitHub App 1. Go to **GitHub > Settings > Developer settings > GitHub Apps** 2. Click **New GitHub App** 3. Fill in the required fields: | Field | Value | | ------------------- | ------------------------------------------------ | | **GitHub App name** | Choose a unique name (e.g., "YourCompany BRMS") | | **Homepage URL** | Your BRMS instance URL | | **Callback URL** | `{APP_URL}/api/app-integrations/github/callback` | ### Step 2: Configure permissions Under **Repository permissions**, set: | Permission | Access Level | | ------------- | ------------ | | Contents | Read & Write | | Pull requests | Read & Write | ### Step 3: Configure installation settings 1. Check **Request user authorization (OAuth) during installation** 2. Under "Where can this GitHub App be installed?", choose: * **Only on this account** - For single organization use * **Any account** - If multiple organizations will use the integration ### Step 4: Generate credentials After creating the app: 1. Note the **App ID** at the top of the settings page 2. Note the **Client ID** in the app settings 3. Generate a **Client Secret** and save it 4. Scroll to **Private keys** and click **Generate a private key** 5. Download the `.pem` file and Base64 encode it: ```bash theme={null} base64 -i your-app-name.private-key.pem ``` ### Step 5: Set GitHub environment variables ```bash theme={null} # GitHub App credentials GITHUB_APP_ID=123456 GITHUB_APP_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQo... GITHUB_APP_CLIENT_ID=Iv1.abc123def456 GITHUB_APP_CLIENT_SECRET=your_client_secret_here # GitHub App installation URL (found in app settings under "Public link") GITHUB_APP_INSTALL_URL=https://github.com/apps/your-app-name/installations/new ``` ## GitLab configuration GitLab integration uses OAuth 2.0 and supports both GitLab.com and self-hosted instances. GitLab integration requires [secrets management](/developers/deployment/brms/secrets-management) to be configured. GitLab credentials (Application ID and Secret) are encrypted and stored using your configured secrets provider. Without secrets management, GitLab integration cannot be enabled. ### Step 1: Create an OAuth application Navigate to your GitLab instance and create an application at one of these locations: | Location | Use case | | --------------------------------- | -------------------------------- | | **User Settings > Applications** | Personal use | | **Group Settings > Applications** | Organization/group use | | **Admin Area > Applications** | Instance-wide (self-hosted only) | ### Step 2: Configure the application | Field | Value | | ---------------- | ------------------------------------------------ | | **Name** | BRMS Integration | | **Redirect URI** | `{APP_URL}/api/app-integrations/gitlab/callback` | | **Confidential** | Yes (checked) | | **Scopes** | `api`, `read_user`, `write_repository` | ### Step 3: Save credentials After creating the application, GitLab displays: * **Application ID** - Save this * **Secret** - Save this (shown only once) Unlike GitHub, GitLab credentials are entered through the BRMS UI during connection, not as environment variables. They are encrypted and stored using your secrets provider. No additional environment variables are required beyond those set in Step 1. ## Azure DevOps configuration Azure DevOps integration authenticates directly against Microsoft Entra ID with a client-credentials grant (service principal), or with a Personal Access Token. There is no OAuth redirect flow: no callback URL has to be registered on the Azure side, and no Azure DevOps-specific environment variables are needed. Like GitLab, Azure DevOps requires [secrets management](/developers/deployment/brms/secrets-management) to be configured. The client secret or PAT is encrypted and stored using your secrets provider. ### Step 1: Choose an authentication method | Method | When to use | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Service Principal** | Recommended for Azure DevOps Services - an Entra ID app registration acting as a machine identity that does not expire with a user | | **Access Token (PAT)** | The only option for Azure DevOps Server (on-premises), but tied to a user account and subject to expiry | ### Step 2 (Service Principal): Create an Entra ID app registration 1. In the Azure portal, go to **Microsoft Entra ID > App registrations > New registration**. No redirect URI is needed. 2. Under **Certificates & secrets**, create a **client secret** and save its value. 3. In Azure DevOps, go to **Organization settings > Users** and add the application as a member, then grant it access to the projects and repositories it should reach. 4. Collect the three values entered in the BRMS connection form: | Field | Where to find it | | --------------------------- | ---------------------------- | | **Directory (tenant) ID** | App registration overview | | **Application (client) ID** | App registration overview | | **Client secret** | The secret created in step 2 | ### Step 2 (Access Token): Create a PAT In Azure DevOps, go to **User settings > Personal access tokens** and create a token with: | Scope | Access Level | | ----- | -------------- | | Code | Read & write | | Build | Read & execute | ### Step 3: Connect from BRMS All credentials are entered through the BRMS UI during connection - the **Organization URL** (`https://dev.azure.com/` for Azure DevOps Services, or your collection URL for Azure DevOps Server) plus the service principal fields or the PAT. See [Git Sync](/brms/setup/git-sync#connecting-azure-devops) for the connection flow. ## Environment variables reference ### Required for all integrations | Variable | Description | | ------------------------- | ----------------------------------------- | | `APP_URL` | BRMS instance URL for OAuth callbacks | | `APP_INTEGRATIONS_SECRET` | OAuth state signing secret (min 32 chars) | ### GitHub-specific | Variable | Description | | -------------------------- | -------------------------- | | `GITHUB_APP_ID` | GitHub App ID | | `GITHUB_APP_PRIVATE_KEY` | Base64-encoded private key | | `GITHUB_APP_CLIENT_ID` | OAuth Client ID | | `GITHUB_APP_CLIENT_SECRET` | OAuth Client Secret | | `GITHUB_APP_INSTALL_URL` | App installation URL | ### GitLab-specific GitLab credentials are entered via the UI and stored encrypted in the database. No additional environment variables are required beyond `APP_INTEGRATIONS_SECRET`. ### Azure DevOps-specific Azure DevOps credentials are entered via the UI and stored encrypted using your secrets provider. No additional environment variables are required beyond `APP_INTEGRATIONS_SECRET`. ## Troubleshooting ### "Integration not configured" message The integration card shows "Not configured" when required environment variables are missing. Verify all required variables are set and restart the API server. ### GitHub: "App not installed" error The GitHub App must be installed on the organization or account you want to connect. Users can install the app during the connection flow. ### GitLab: "Invalid redirect URI" error The Redirect URI in your GitLab OAuth application must exactly match: ``` {APP_URL}/api/app-integrations/gitlab/callback ``` ### GitLab: "URL must use HTTPS" error GitLab requires HTTPS for OAuth applications. Ensure your GitLab URL starts with `https://`. ### Azure DevOps: "Identity is not a member of this Azure DevOps organization" Azure DevOps reports a service principal that was never added to the organization as error `TF401444`, with a misleading suggestion to "sign in via a web browser" - which a service principal cannot do. The actual fix: add the application under **Organization settings > Users** in Azure DevOps and grant it access to the relevant projects. ### GitLab: "Secrets management required" error GitLab integration requires secrets management to be configured. See [Secrets management](/developers/deployment/brms/secrets-management) to set up a secrets provider. # BRMS overview Source: https://docs.gorules.io/developers/deployment/brms/overview Self-hosted business rules management system with visual editor and REST API. The GoRules BRMS is a complete business rules management system that provides a visual editor for creating and managing decision models, version control, release management, and REST API for rule evaluation. Available on Docker Hub: `gorules/brms` Obtain your license key ## When to use BRMS **Choose BRMS when:** * Business users need to create and edit rules without code changes * You need version control and audit trails for rule changes * Multiple environments (dev, staging, prod) require release management * You want a web-based UI for rule authoring and testing **Consider alternatives when:** * You only need to evaluate rules (use [Agent](/developers/deployment/agent/overview) or [embedded SDK](/developers/deployment/embedded)) * Rules are managed entirely in code ## Architecture ```mermaid theme={null} flowchart TB users["Business Users"] --> brms brms["GoRules BRMS
(Web UI + API)"] postgres["PostgreSQL"] storage["Object Storage
(S3, GCS, Azure)"] agent["GoRules Agent"] apps["Your Applications"] brms --> postgres brms --> storage agent --> storage apps --> agent ``` ## Requirements | Component | Requirement | | --------- | ------------------------------------------------------------ | | Database | PostgreSQL 12+ | | Runtime | Docker (Linux x86\_64) | | License | From [portal.gorules.io](https://portal.gorules.io) | | Network | Outbound HTTPS to `portal.gorules.io` for license validation | ## Environment variables ### Required ```shell theme={null} DB_HOST=your-database-host DB_USER=gorules DB_PASSWORD=your-password DB_NAME=gorules LICENSE_KEY=your-license-key # From portal.gorules.io ``` ### Database ```shell theme={null} DB_HOST=db.example.com # Required DB_PORT=5432 # Default: 5432 DB_USER=gorules # Required DB_PASSWORD=your-password # Required DB_NAME=gorules # Required DB_CREDENTIALS_PROVIDER=default # Options: default, aws-iam, azure-iam ``` ### Database SSL ```shell theme={null} DB_SSL_DISABLED=false # Default: false DB_REJECT_UNAUTHORIZED=true # Default: true DB_SSL_CA= # CA certificate (base64 encoded) DB_SSL_CERT= # Client certificate (raw PEM) DB_SSL_KEY= # Client private key (raw PEM) DB_SSL_ADVANCED= # Advanced SSL config ``` ### Database options ```shell theme={null} DB_MIGRATE=true # Run migrations on startup (default: true) DB_SYNCHRONIZE=false # Auto-sync schema - dev only! (default: false) DB_LOGGING=false # Log SQL queries (default: false) ``` Never enable `DB_SYNCHRONIZE` in production. It can cause data loss. ### Application ```shell theme={null} APP_NAME=GoRules # Display name in UI APP_URL=https://rules.example.com # Public URL of the application API_BASE_URL=/api # API base path (default: /api) HOME_URI=/ # Default landing page (default: /) LICENSE_KEY=your-license-key # Required LICENSE_MODE=online # Options: online, offline (default: online) LOG_LEVEL=info # Options: debug, info, warn, error (default: info) ``` ### HTTP server ```shell theme={null} HTTP_HOST=0.0.0.0 # Default: 0.0.0.0 HTTP_PORT=80 # Default: 80 HTTP_SSL_KEY= # SSL private key HTTP_SSL_CERT= # SSL certificate ``` ### Security ```shell theme={null} COOKIE_SECRET=your-32-byte-secret # Session cookie encryption SESSION_DURATION_MINUTES=1440 # Session timeout (default: 1440 = 24h) HASH_SECRET=your-hash-secret # Internal hashing RELEASE_ZIP_PASSWORD=your-password # Password-protect release downloads ``` Generate a secure secret: ```bash theme={null} openssl rand -hex 32 ``` ### CORS ```shell theme={null} CORS_ALLOW_ORIGIN=https://example.com # Allowed origins (comma-separated, supports regex) ``` Example with multiple origins and regex: ```shell theme={null} CORS_ALLOW_ORIGIN=https://gorules.io,REGEX:\.gorules\.io$ ``` ### Email (SMTP) ```shell theme={null} EMAIL_ENABLED=true # Enable email sending (default: true) EMAIL_HOST=smtp.example.com # SMTP server hostname EMAIL_PORT=587 # SMTP server port EMAIL_SECURE=false # Use TLS/SSL (default: false) EMAIL_AUTH_USER=your-username # SMTP username EMAIL_AUTH_PASS=your-password # SMTP password EMAIL_FROM=noreply@example.com # Sender address (default: noreply@gorules.io) ``` ### Email TLS options ```shell theme={null} EMAIL_TLS_REJECT_UNAUTHORIZED=true # Reject invalid certificates EMAIL_TLS_SERVER_NAME=smtp.example.com # TLS server name EMAIL_TLS_SKIP_SERVER_IDENTITY=false # Skip server identity verification ``` ### Google sign-in ```shell theme={null} AUTH_GOOGLE_ENABLED=true # Enable Sign in with Google (default: false) AUTH_GOOGLE_CLIENT_ID=your-client-id AUTH_GOOGLE_CLIENT_SECRET=your-secret ``` The OAuth redirect URI is `{APP_URL}/oauth/google`. ### Secrets encryption See [Secrets management](/developers/deployment/brms/secrets-management) for detailed setup. ### SSO (OIDC) See [SSO configuration](/developers/deployment/brms/sso) for detailed setup. ```shell theme={null} SSO_OAUTH2_PROVIDER=oidc # Options: azure, okta, oidc SSO_OAUTH2_CLIENT_ID=your-client-id SSO_OAUTH2_CLIENT_SECRET=your-secret # Not required for PKCE SSO_OAUTH2_ISSUER=https://your-idp.com SSO_OAUTH2_JWKS_URI=https://your-idp.com/.well-known/jwks.json SSO_OAUTH2_SCOPES=openid email profile # Default: openid email profile SSO_OAUTH2_REDIRECT_URI=/_callback # Default: /_callback SSO_OAUTH2_AUTH_URL=https://... # Authorization endpoint (legacy providers) SSO_OAUTH2_TOKEN_URL=https://... # Token endpoint (legacy providers) SSO_OAUTH2_AUTHORITY_URL=https://... # Authority URL ``` ### SSO group mapping ```shell theme={null} SSO_OAUTH2_GROUPS_MAPPING=group1->admin,group2->member SSO_OAUTH2_CUSTOM_CLAIM_NAME=groups # JWT claim containing groups (default: groups) SSO_OAUTH2_ROLES_MAPPING_ENABLED=false # Enable fine-grained role mapping (default: false) SSO_OAUTH2_IDENTITY_TOKEN_SOURCE=access_token # Options: access_token, id_token ``` ### LLM integration ```shell theme={null} LLM_PROVIDER=anthropic # Options: anthropic, google, openai, amazon-bedrock, google-vertex, azure-openai, ollama LLM_MODEL=claude-sonnet-4-6 # Model name LLM_MODEL_LOWER=claude-haiku-4-5 # Cheaper model for lightweight tasks (falls back to LLM_MODEL) LLM_API_KEY=your-api-key # Not required for Bedrock, Vertex, Ollama LLM_TEMPERATURE=0.4 # Applies to Gemini/Google providers only (default: 0.4) LLM_CONTEXT_WINDOW=200000 # Context window size in tokens (provider default) LLM_MAX_OUTPUT_TOKENS=32000 # Maximum tokens per response (default: 32000) LLM_THINKING_LEVEL=medium # Extended thinking: high, medium, low (default: medium) ``` ## Health probes **Path:** `/api/health` **Port:** `80` Recommended probe configuration: | Setting | Value | | ------------- | ---------- | | Initial delay | 10 seconds | | Period | 10 seconds | ## API reference See the [API reference](/api-reference/introduction) for endpoint documentation. # Secrets management Source: https://docs.gorules.io/developers/deployment/brms/secrets-management Configure envelope encryption with KEK/DEK hierarchy to protect deployment credentials and user-defined secrets. GoRules BRMS uses **envelope encryption** with a KEK/DEK (Key Encryption Key / Data Encryption Key) hierarchy to protect sensitive data such as deployment credentials and user-defined secrets. ## Architecture ```mermaid theme={null} flowchart TB KEK["KEK (Master Key)
AWS KMS / Azure Key Vault /
GCP KMS / Environment"] KEK -->|wraps/unwraps| DEK_A["DEK-A
(Organisation A)"] KEK -->|wraps/unwraps| DEK_B["DEK-B
(Organisation B)"] DEK_A -->|encrypts/decrypts| Secrets_A["Secrets A"] DEK_B -->|encrypts/decrypts| Secrets_B["Secrets B"] ``` **Flow:** 1. KEK (in KMS) wraps/unwraps the DEKs 2. Each organisation has its own DEK 3. DEK encrypts/decrypts that organisation's secrets 4. Secrets are stored encrypted in the database ## Key hierarchy | Key | Purpose | Storage | | -------------------- | ------------------------- | ---------------------------------------- | | **KEK** (Master Key) | Wraps/unwraps DEKs | External KMS or environment variable | | **DEK** (Data Key) | Encrypts/decrypts secrets | Encrypted in database (per organisation) | ## Cryptographic specifications | Component | Algorithm | | ----------------- | ------------------------------------------------------------------- | | Secret encryption | AES-256-GCM | | DEK wrapping | Provider-managed (AWS KMS, Azure Key Vault, GCP KMS) or AES-256-GCM | ## Multi-tenant isolation Each organisation has its own unique DEK (Data Encryption Key): * **Cryptographic isolation**: Organisation A's DEK cannot decrypt Organisation B's secrets * **Breach containment**: If one DEK is compromised, only that organisation's secrets are affected * **No cross-tenant access**: Even with database access, secrets from other organisations remain encrypted with different keys ## Key management The KEK (master key) must never be deleted or changed. If the KEK is lost or changed, **all DEKs become unrecoverable** and all encrypted secrets are permanently lost. **Best practices:** * **Restrict access**: Only automated systems should have access to the KEK. Human access should be emergency-only. * **Never delete**: Configure key deletion protection in your KMS provider. * **Backup carefully**: If using environment variable provider, ensure the master key is securely backed up. * **Audit access**: Enable KMS audit logging to track all key operations. | KMS Provider | Recommended Settings | | --------------- | ------------------------------------------------------------------------------------ | | AWS KMS | Enable key deletion protection, restrict IAM to `kms:Encrypt` and `kms:Decrypt` only | | Azure Key Vault | Enable purge protection, use RBAC with minimal permissions | | GCP KMS | Set key destruction duration, restrict IAM roles | ## Configuration ### Provider selection Set `SECRETS_PROVIDER` to choose the encryption backend: | Provider | Value | Description | | -------------------- | ---------------- | ------------------------------------------------ | | Environment Variable | `env` | Master key from environment (simple deployments) | | AWS KMS | `aws-kms` | AWS Key Management Service | | Azure Key Vault | `azure-keyvault` | Azure Key Vault | | GCP KMS | `gcp-kms` | Google Cloud KMS | ### Environment variables #### Provider selection (required) | Variable | Description | | ------------------ | ----------------------------------------------------------------- | | `SECRETS_PROVIDER` | Provider to use: `env`, `aws-kms`, `azure-keyvault`, or `gcp-kms` | #### Environment variable provider (`env`) | Variable | Description | | -------------------- | ------------------------------------------------------------ | | `SECRETS_MASTER_KEY` | Master key passphrase (any string, min 32 chars recommended) | #### AWS KMS provider (`aws-kms`) | Variable | Description | | ------------------------ | ------------------------------------------------- | | `SECRETS_AWS_KMS_KEY_ID` | AWS KMS key ID or ARN | | `SECRETS_AWS_KMS_REGION` | AWS region (optional, falls back to `AWS_REGION`) | #### Azure Key Vault provider (`azure-keyvault`) | Variable | Description | | --------------------------------- | ------------------------------------------------------- | | `SECRETS_AZURE_KEYVAULT_URL` | Key Vault URL (e.g., `https://myvault.vault.azure.net`) | | `SECRETS_AZURE_KEYVAULT_KEY_NAME` | Key name in the vault | #### GCP KMS provider (`gcp-kms`) | Variable | Description | | -------------------------- | -------------------------------------------------------------------- | | `SECRETS_GCP_KMS_KEY_NAME` | Full resource name: `projects/*/locations/*/keyRings/*/cryptoKeys/*` | #### Cache settings | Variable | Default | Description | | ------------------------------- | ------- | ------------------------------------------ | | `SECRETS_DEK_CACHE_TTL_MINUTES` | `240` | How long to cache decrypted DEKs in memory | ### Example configurations ```bash AWS KMS theme={null} SECRETS_PROVIDER=aws-kms SECRETS_AWS_KMS_KEY_ID=arn:aws:kms:us-east-1:123456789:key/12345-abcd-6789 SECRETS_AWS_KMS_REGION=us-east-1 ``` ```bash Azure Key Vault theme={null} SECRETS_PROVIDER=azure-keyvault SECRETS_AZURE_KEYVAULT_URL=https://mycompany-vault.vault.azure.net SECRETS_AZURE_KEYVAULT_KEY_NAME=gorules-master-key ``` ```bash GCP KMS theme={null} SECRETS_PROVIDER=gcp-kms SECRETS_GCP_KMS_KEY_NAME=projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/gorules-key ``` ```bash Environment Variable theme={null} SECRETS_PROVIDER=env SECRETS_MASTER_KEY=your-secure-passphrase-at-least-32-characters ``` ## Limits | Limit | Value | | -------------------------------- | ----- | | Maximum secret value size | 32 KB | | Maximum secrets per organisation | 1,000 | # Single sign-on Source: https://docs.gorules.io/developers/deployment/brms/sso Authenticate to GoRules BRMS using any OIDC provider. GoRules BRMS supports single sign-on (SSO) through OIDC providers, allowing users to authenticate with existing corporate credentials. SSO also supports mapping identity provider groups to BRMS permissions. SSO is available on Business plan and above. ## OIDC PKCE (recommended) The recommended approach uses standard OIDC with PKCE flow. This method works with any OIDC-compliant identity provider and has been tested with Microsoft Entra ID, Okta, Keycloak, and PingOne. ### Step 1: Create an OIDC application Create a new OIDC/OAuth application in your identity provider. Configure it as a **public client** (SPA) with PKCE enabled. Set the redirect URI to: ``` {YOUR_APP_URL}/_callback ``` For provider-specific instructions: * [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) * [Okta](https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/#set-up-your-app) * [Keycloak](https://www.keycloak.org/docs/latest/server_admin/#_oidc_clients) * [PingOne](https://docs.pingidentity.com/pingone/applications/p1_applications_add_applications.html) ### Step 2: Configure BRMS Set these environment variables with values from your OIDC application: ```shell theme={null} SSO_OAUTH2_PROVIDER=oidc SSO_OAUTH2_CLIENT_ID=your-client-id SSO_OAUTH2_ISSUER=https://your-idp.example.com SSO_OAUTH2_JWKS_URI=https://your-idp.example.com/.well-known/jwks.json ``` Always set `SSO_OAUTH2_JWKS_URI` in production. Without it, BRMS cannot validate token signatures. ### Provider examples ```shell Okta theme={null} SSO_OAUTH2_PROVIDER=oidc SSO_OAUTH2_CLIENT_ID=0oaycgerypftkPwxE697 SSO_OAUTH2_ISSUER=https://your-domain.okta.com/oauth2/default SSO_OAUTH2_JWKS_URI=https://your-domain.okta.com/oauth2/default/v1/keys ``` ```shell Keycloak theme={null} SSO_OAUTH2_PROVIDER=oidc SSO_OAUTH2_CLIENT_ID=brms SSO_OAUTH2_ISSUER=https://keycloak.example.com/realms/your-realm SSO_OAUTH2_JWKS_URI=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/certs ``` ```shell Microsoft Entra ID theme={null} SSO_OAUTH2_PROVIDER=oidc SSO_OAUTH2_CLIENT_ID=your-application-client-id SSO_OAUTH2_ISSUER=https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0 SSO_OAUTH2_JWKS_URI=https://login.microsoftonline.com/YOUR_TENANT_ID/discovery/v2.0/keys ``` ```shell PingOne theme={null} SSO_OAUTH2_PROVIDER=oidc SSO_OAUTH2_CLIENT_ID=your-application-client-id SSO_OAUTH2_ISSUER=https://auth.pingone.eu/YOUR_ENVIRONMENT_ID/as SSO_OAUTH2_JWKS_URI=https://auth.pingone.eu/YOUR_ENVIRONMENT_ID/as/jwks ``` ### Additional options | Variable | Description | Default | | ---------------------------------- | ---------------------------------------- | ---------------------- | | `SSO_OAUTH2_SCOPES` | OAuth2 scopes to request | `openid email profile` | | `SSO_OAUTH2_REDIRECT_URI` | Callback path | `/_callback` | | `SSO_OAUTH2_AUTHORITY_URL` | Authority URL (if different from issuer) | - | | `SSO_OAUTH2_IDENTITY_TOKEN_SOURCE` | Token for identity claims | `access_token` | | `SSO_OAUTH2_CUSTOM_CLAIM_NAME` | JWT claim containing groups | `groups` | ## Azure AD (legacy) This is a legacy integration that uses internal sessions. For new deployments, use [OIDC PKCE](#oidc-pkce-recommended) with Microsoft Entra ID instead. ### 1. Create an Enterprise Application 1. Log in to the [Azure Portal](https://portal.azure.com) and navigate to **Entra ID** 2. Select **Enterprise applications** → **New application** 3. Click **Create your own application** 4. Choose **Register an application to integrate with Microsoft Entra ID** 5. Name the application (e.g., `GoRules BRMS`) 6. Select **Accounts in this organizational directory only** (single tenant) 7. Set Redirect URI to **Web** and enter `https://your-brms-url/oauth/azure` 8. After creation, navigate to **Single sign-on** → **Go to application** 9. Copy the **Application (client) ID** ### 2. Add group claims (optional) 1. In App registrations, open **Token configuration** 2. Click **Add group claim** 3. Configure claims according to your requirements 4. For Access and ID tokens, select **sAMAccountName** 5. Save ### 3. Create credentials 1. Open **Certificates & secrets** 2. Under Client secrets, click **New client secret** 3. Add description, set expiry, click **Add** 4. Copy the secret value immediately (not visible after leaving the page) ### 4. Copy endpoints 1. In the Overview section, click **Endpoints** 2. Copy: * OAuth 2.0 authorization endpoint (v2) * OAuth 2.0 token endpoint (v2) * Authority URL: `https://login.microsoftonline.com/TENANT_ID` ### 5. Configure BRMS ```shell theme={null} SSO_OAUTH2_PROVIDER=azure SSO_OAUTH2_CLIENT_ID= SSO_OAUTH2_CLIENT_SECRET= SSO_OAUTH2_SCOPES=openid email profile SSO_OAUTH2_AUTH_URL= SSO_OAUTH2_TOKEN_URL= SSO_OAUTH2_AUTHORITY_URL=https://login.microsoftonline.com/TENANT_ID APP_URL=https://your-brms-url EMAIL_ENABLED=false ``` ## Okta (legacy) This is a legacy integration that uses internal sessions. For new deployments, use [OIDC PKCE](#oidc-pkce-recommended) with Okta instead. ### 1. Create an application 1. Log in to your Okta Admin dashboard 2. Navigate to **Applications** → **Create App Integration** 3. Select **OIDC - OpenID Connect** and **Web Application** 4. Name the application (e.g., `GoRules BRMS`) 5. Set grant type to **Authorization Code** 6. Set Sign-in redirect URI to `https://your-brms-url/oauth/okta` 7. Leave Sign-out redirect URIs empty 8. Assign to necessary user groups 9. Save If Federation Broker Mode is shown, disable it. ### 2. Add group claims (optional) 1. In the application, open **Sign On** tab 2. In **OpenID Connect ID Token**, click **Edit** 3. Set Group Claim Type to **Filter** 4. Enter `groups`, select **Matches regex**, enter `.*` 5. Save ### 3. Copy credentials From the General tab, copy: * Client ID * Client Secret ### 4. Configure BRMS ```shell theme={null} SSO_OAUTH2_PROVIDER=okta SSO_OAUTH2_CLIENT_ID= SSO_OAUTH2_CLIENT_SECRET= SSO_OAUTH2_SCOPES=openid email profile SSO_OAUTH2_AUTH_URL=https://your-domain.okta.com/oauth2/v1/authorize SSO_OAUTH2_TOKEN_URL=https://your-domain.okta.com/oauth2/v1/token APP_URL=https://your-brms-url EMAIL_ENABLED=false ``` See [Okta documentation](https://developer.okta.com/docs/reference/api/oidc/#composing-your-base-url) for composing your base URL. ## Group mapping Map identity provider groups to BRMS roles using `SSO_OAUTH2_GROUPS_MAPPING`. The format is `{group}->{role}` comma-separated. ### Global roles Four global roles are available: `owner`, `admin`, `author`, and `member`. ```shell theme={null} SSO_OAUTH2_CUSTOM_CLAIM_NAME=groups SSO_OAUTH2_GROUPS_MAPPING=Admins->admin,Developers->author,Users->member ``` Users not in any mapped group receive `member` permissions. ### Provider-specific formats **Azure AD** uses group UUIDs: ```shell theme={null} SSO_OAUTH2_SCOPES=openid email profile groups SSO_OAUTH2_GROUPS_MAPPING=77777777-7777-7777-7777-777777777777->admin,88888888-8888-8888-8888-888888888888->author ``` **Okta** uses group names: ```shell theme={null} SSO_OAUTH2_SCOPES=openid email profile groups SSO_OAUTH2_GROUPS_MAPPING=Admins->admin,Developers->author,Users->member ``` **Keycloak** uses nested claims: ```shell theme={null} SSO_OAUTH2_CUSTOM_CLAIM_NAME=resource_access.account.roles SSO_OAUTH2_GROUPS_MAPPING=admin_role->admin,user_role->member ``` ### Fine-grained per-project mapping For granular control per project, enable role mapping in BRMS: ```shell theme={null} SSO_OAUTH2_ROLES_MAPPING_ENABLED=true ``` Then configure roles through the BRMS UI: 1. Navigate to **Settings** → **Users & Roles** → **Roles** 2. Create or edit a role 3. Configure: * **Role Name**: Custom identifier * **IDP Groups**: Map SSO groups to this role * **Project Access**: Assign specific projects * **Permissions**: Define granular permissions ### Comparison | Feature | Global mapping | Per-project mapping | | ----------------- | --------------------------- | --------------------------------------- | | Configuration | Environment variables | BRMS UI | | Granularity | System-wide | Per-project | | Use case | Simple admin/member split | Complex multi-project permissions | | Required variable | `SSO_OAUTH2_GROUPS_MAPPING` | `SSO_OAUTH2_ROLES_MAPPING_ENABLED=true` | Use both methods together: environment variable mapping for global admins, and per-project mapping for fine-grained control. Global admins have access to every project. ## Troubleshooting ### Invalid redirect URI The redirect URI must exactly match the one registered with your IdP: * OIDC PKCE (recommended): `https://your-brms-url/_callback` * Azure (legacy): `https://your-brms-url/oauth/azure` * Okta (legacy): `https://your-brms-url/oauth/okta` ### Groups not syncing * Verify groups claim is included in tokens * Check `SSO_OAUTH2_CUSTOM_CLAIM_NAME` matches your IdP's claim name * Ensure users are assigned to the mapped groups in your IdP # Embedded SDK deployment Source: https://docs.gorules.io/developers/deployment/embedded Bundle the rules engine directly into your application for maximum performance. Embed the ZEN Engine directly in your application using native SDKs. This deployment model provides the highest performance with no network overhead. ## When to use embedded **Choose embedded when you need:** * Maximum evaluation performance (sub-millisecond latency) * Offline capability * No external service dependencies * Direct control over the engine lifecycle **Consider alternatives when:** * Multiple services need the same rules * You want centralized rule management * Business users need to update rules without deployments ## Architecture ```mermaid theme={null} flowchart TB subgraph app["Your Application"] subgraph sdk["ZEN Engine SDK"] decision["Decision (JDM)"] end end ``` The engine runs in-process. Decision files are loaded at startup or runtime. ## SDKs ## Loading decisions ### Bundle with your application Include decision files in your deployment package: Load at application startup: ```javascript theme={null} import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine'; import fs from 'fs'; import path from 'path'; const rulesDir = path.join(__dirname, 'rules'); // Precompile decisions into a cache - ZenDecisionContent compiles for better performance const decisionCache = new Map(); decisionCache.set('pricing.json', new ZenDecisionContent( fs.readFileSync(path.join(rulesDir, 'pricing.json')) )); decisionCache.set('eligibility.json', new ZenDecisionContent( fs.readFileSync(path.join(rulesDir, 'eligibility.json')) )); // Create engine with a loader that returns precompiled decisions const engine = new ZenEngine({ loader: async (key) => decisionCache.get(key), }); // Evaluate by decision name const result = await engine.evaluate('pricing.json', { customer: { tier: 'gold' } }); ``` ### Fetch from remote storage Load decisions from S3, GCS, or HTTP endpoints: ```javascript theme={null} import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine'; const decisionCache = new Map(); // Loader fetches and precompiles decisions on demand const loader = async (key) => { if (decisionCache.has(key)) { return decisionCache.get(key); } const response = await fetch(`${process.env.RULES_URL}/${key}`); const buffer = Buffer.from(await response.arrayBuffer()); const content = new ZenDecisionContent(buffer); decisionCache.set(key, content); return content; }; const engine = new ZenEngine({ loader }); // Evaluate - loader fetches and caches automatically const result = await engine.evaluate('pricing.json', { customer: { tier: 'gold' } }); ``` ### Hot reloading Update decisions without restarting: ```javascript theme={null} import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine'; class DecisionManager { constructor() { this.cache = new Map(); this.engine = new ZenEngine({ loader: async (key) => this.cache.get(key), }); } load(name, buffer) { // Precompile and cache this.cache.set(name, new ZenDecisionContent(buffer)); } reload(name, buffer) { // Atomic swap - precompile new content and replace this.cache.set(name, new ZenDecisionContent(buffer)); } async evaluate(name, input) { return this.engine.evaluate(name, input); } } ``` ## Best practices **Initialize once** - Create engine and decisions at startup, not per-request. **Handle errors gracefully** - Decision loading can fail. Have fallback behavior. **Version your rules** - Track which rule version is deployed with your application. **Test thoroughly** - Rule changes ship with code changes. Include rule tests in your CI/CD. ## Updating rules With embedded deployment, updating rules requires redeploying your application: 1. Export updated rules from BRMS 2. Add to your repository 3. Deploy application with new rules For more dynamic updates, consider the [Agent deployment](/developers/deployment/agent) model. # Developer tools Source: https://docs.gorules.io/developers/developer-tools Call any graph or policy as an HTTP endpoint, generate clients from OpenAPI, and bridge your workspace to MCP. Open **Developer Tools** from the project sidebar. Every graph and policy in the scope you're viewing - branch, commit, release, or environment - is callable as an HTTP endpoint. The page lists each document's endpoint, request and response schemas, ready-made payloads, and integration snippets. Developer Tools page listing graph and policy endpoints with request, response, and integration tabs ## Evaluate a decision All evaluation endpoints accept `POST` with the input wrapped in `context`, and authenticate with a project access token in the `X-Access-Token` header: ```bash cURL theme={null} curl -X POST "https://acme.us1.gorules.io/api/rules/acme-lending/evaluate/loan-approval" \ -H "X-Access-Token: $GORULES_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "context": { "applicant": { "employment": "EMPLOYED", "monthlyIncome": 6500, "monthlyDebt": 1200, "creditScore": 760 }, "loan": { "amount": 25000, "termMonths": 60 } }, "trace": false }' ``` ```javascript Node.js theme={null} const response = await fetch( "https://acme.us1.gorules.io/api/rules/acme-lending/evaluate/loan-approval", { method: "POST", headers: { "X-Access-Token": process.env.GORULES_TOKEN, "Content-Type": "application/json", }, body: JSON.stringify({ context: { applicant: { employment: "EMPLOYED", monthlyIncome: 6500, monthlyDebt: 1200, creditScore: 760 }, loan: { amount: 25000, termMonths: 60 }, }, trace: false, }), }, ); if (!response.ok) { throw new Error(`Evaluation failed: ${response.status}`); } const { result } = await response.json(); ``` ```python Python theme={null} import os import requests response = requests.post( "https://acme.us1.gorules.io/api/rules/acme-lending/evaluate/loan-approval", headers={"X-Access-Token": os.environ["GORULES_TOKEN"]}, json={ "context": { "applicant": {"employment": "EMPLOYED", "monthlyIncome": 6500, "monthlyDebt": 1200, "creditScore": 760}, "loan": {"amount": 25000, "termMonths": 60}, }, "trace": False, }, ) response.raise_for_status() result = response.json()["result"] ``` Response: ```json theme={null} { "performance": "156.2µs", "result": { "status": "approved", "approved": true, "reason": "Approved at standard rate", "debtToIncome": 0.18 } } ``` Set `"trace": true` to include a per-node `trace` object with each node's input, output, and execution time. ## Endpoints `{projectId}` accepts either the project ID or the project key (`acme-lending` above). `{path}` is the document's path within the project, for example `loan-approval` or `billing/invoice-validation`. | Source | Endpoint | | ---------------- | ----------------------------------------------------------------- | | Main branch head | `POST /api/rules/{projectId}/evaluate/{path}` | | Branch head | `POST /api/rules/{projectId}/branch/{branchId}/evaluate/{path}` | | Commit | `POST /api/rules/{projectId}/commit/{commitId}/evaluate/{path}` | | Release | `POST /api/rules/{projectId}/release/{releaseId}/evaluate/{path}` | | Environment | `POST /api/rules/{projectId}/env/{environmentId}/evaluate/{path}` | A `GET` on each source root (the same URL without `/evaluate/{path}`) returns the OpenAPI document for that source. Point production integrations at an environment: [deploying a release](/brms/deploy/environments) swaps the rules behind the endpoint with no client changes. Branch and commit endpoints evaluate draft content, which is useful for CI and pre-production testing. The [Agent](/developers/deployment/agent/overview) serves the same surface for the release it has loaded: `POST /api/rules/{projectId}/evaluate/{path}` and `GET /api/rules/{projectId}`. Requests, responses, and the OpenAPI document match BRMS, so integrations move between the two without client changes. The Agent requires a token only when the deployed release ships evaluation tokens; if none are scoped to the deployed target, it serves requests without authentication. The legacy `/api/projects/{projectId}/evaluate/{path}` endpoints remain fully supported on both BRMS and the Agent, so existing integrations keep working unchanged. ## API explorer The **Target** selector switches every endpoint, schema, and snippet on the page between sources: `Branch: main`, any other branch, a commit, a release, an environment, or `MCP Server (local)`. Each endpoint expands into four tabs: | Tab | Contents | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Request | Input fields inferred from the document, with types and required flags; the body wraps them as `{ "context": … }` | | Response | Output schema inferred from the document | | Examples | Up to three payloads taken from test cases that reference the document, or a skeleton generated from the input schema when no tests exist | | Integrate | Copy-paste snippets for cURL, Node.js, Python, Go, Rust, C#, Java, and Kotlin | The **Native** toggle on the Integrate tab switches snippets from HTTP calls to the embedded ZEN Engine, evaluating the same document in-process with the [SDKs](/developers/sdks/nodejs). ## Evaluation tokens Evaluation endpoints authenticate with project access tokens, generated in project settings. A token is either **Full access** or restricted with **Custom scopes**: | Allowed action | Grants | | -------------- | --------------------------------------- | | Evaluate | Run rules via the evaluation endpoints | | List rules | Fetch the OpenAPI document for a source | | Store traces | Persist evaluation traces | Custom-scoped tokens also narrow which evaluation targets they may hit: Branch/Commit (the main-branch head, any branch head, and pinned commits), Releases, or a specific environment. Nothing selected means all targets. A token without scopes has full access. On the API page, **Evaluation tokens** lists the tokens valid for the currently selected target, so you can copy one that will actually authorize the request. Scope production tokens to a single environment with only the Evaluate action. A leaked token then can't read rule definitions or evaluate draft content. ## OpenAPI spec The **OpenAPI spec** dropdown offers **Download JSON**, **Copy spec URL**, and **Copy cURL** for the selected target. The same document is served by the API: ```bash theme={null} curl "https://acme.us1.gorules.io/api/rules/{projectId}/env/{environmentId}" \ -H "X-Access-Token: $GORULES_TOKEN" ``` The spec contains one operation per graph and policy, with input and output JSON Schemas derived from the documents, examples pulled from test cases, and security declared as an `apiKey` scheme on the `X-Access-Token` header. Each operation carries an `x-gorules` extension with the document `path`, `kind` (graph or policy), a `contentHash`, `hasInputSchema`, and `hasOutputSchema` - enough metadata to drive client generators or your own tooling. ### Generate typed clients Because each operation's `context` schema is derived from the document's input schema, any OpenAPI code generator turns the spec into compile-time types for your rules. With [openapi-typescript](https://openapi-ts.dev): ```bash theme={null} curl -H "X-Access-Token: $GORULES_TOKEN" \ "https://acme.us1.gorules.io/api/rules/acme-lending/env/production" -o rules-spec.json npx openapi-typescript rules-spec.json -o rules.d.ts ``` Then evaluate with full type safety - wrong field names or types in `context` fail at compile time: ```typescript theme={null} import createClient from 'openapi-fetch'; import type { paths } from './rules'; const client = createClient({ baseUrl: 'https://acme.us1.gorules.io/api/rules/acme-lending/env/production', headers: { 'X-Access-Token': process.env.GORULES_TOKEN }, }); const { data } = await client.POST('/evaluate/loan-approval', { body: { context: { applicant: { employment: 'EMPLOYED', monthlyIncome: 6500, monthlyDebt: 1200, creditScore: 760 }, loan: { amount: 25000, termMonths: 60 }, }, }, }); ``` The same spec feeds generators for other languages, and the generated context types are equally useful when evaluating in-process with the [embedded SDKs](/developers/sdks/nodejs). Point the generator at a release or environment source so types match what production serves, and use each operation's `x-gorules.contentHash` to detect when rules changed and types need regenerating - a natural CI step. Fields typed with a [dictionary](/brms/build/policies#dictionaries) appear in the spec as `{"$dictionary": "name"}` - a GoRules extension, not standard JSON Schema. Evaluation enforces the dictionary's values, but code generators don't recognise the keyword and type such fields loosely. ## MCP server The **MCP Server** tab in Developer Tools bridges the browser workspace to AI agents such as Claude Code and Cursor through a local MCP endpoint. ```bash theme={null} npx @gorules/cli mcp start ``` The CLI starts on port `41919` and prints a one-time 8-character token. Paste it into the **MCP Server** tab; the browser asks once for Local Network Access permission. The session status moves from Not connected through Verifying to Connected (or Blocked if the browser denies local network access). A connected MCP client can: * Read workspace files for the scope you're viewing - drafts, releases, or change requests. * Use the same tools as GoRules AI: add nodes, edit decision tables, plan changes. * Evaluate decisions with trace, performance, and intermediate state. Traffic stays on `localhost:41919` and tokens rotate every CLI session. For CLI flags and client configuration, see [GoRules CLI](/developers/cli) and [MCP integration](/developers/mcp). # AWS Glue Rules Engine Source: https://docs.gorules.io/developers/integrations/aws-glue Run distributed rule evaluation as managed AWS Glue jobs. Process large datasets with GoRules on AWS Glue's serverless Spark infrastructure. ## Setup ### 1. Create requirements file Create `requirements.txt` and upload to S3: ``` zen-engine ``` ### 2. Configure Glue job parameters | Parameter | Value | | ----------------------------- | ------------ | | `--additional-python-modules` | `zen-engine` | Or for requirements file: | Parameter | Value | | ----------------------------------- | ----------------------------------- | | `--python-modules-installer-option` | `-r` | | `--additional-python-modules` | `s3://your-bucket/requirements.txt` | ### 3. IAM permissions Ensure your Glue job role has access to: * S3 buckets containing rules and data * Any other AWS services your rules might reference ## Singleton evaluator Use a singleton pattern with precompiled `ZenDecisionContent` for optimal performance: ```python theme={null} import zen class ZenEvaluator: """ Singleton for ZenEngine with precompiled ZenDecisionContent. Broadcast dict[str, str] (picklable), precompile to dict[str, ZenDecisionContent], then engine.evaluate() uses the loader to return precompiled content. """ _engine: zen.ZenEngine | None = None _content: dict[str, zen.ZenDecisionContent] = {} _raw: dict[str, str] = {} @classmethod def initialize(cls, loaders: dict[str, str]): """Precompile loaders dict to ZenDecisionContent.""" if cls._raw != loaders: cls._raw = loaders cls._content = {k: zen.ZenDecisionContent(v) for k, v in loaders.items()} cls._engine = None @classmethod def _get_engine(cls) -> zen.ZenEngine: if cls._engine is None: def loader(key: str) -> zen.ZenDecisionContent: return cls._content[key] cls._engine = zen.ZenEngine({"loader": loader}) return cls._engine @classmethod def evaluate(cls, key: str, context: dict) -> dict: """Evaluate a decision by key with given context.""" return cls._get_engine().evaluate(key, context) ``` ## Basic job ```python theme={null} import sys import boto3 import json from awsglue.utils import getResolvedOptions from pyspark.context import SparkContext from awsglue.context import GlueContext from awsglue.job import Job from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType # Initialize Glue context args = getResolvedOptions(sys.argv, ['JOB_NAME']) sc = SparkContext() glueContext = GlueContext(sc) spark = glueContext.spark_session job = Job(glueContext) job.init(args['JOB_NAME'], args) logger = glueContext.get_logger() # Load all decisions from S3 zip at startup import zipfile import io s3 = boto3.client('s3') response = s3.get_object(Bucket='your-bucket', Key='decisions.zip') zip_bytes = response['Body'].read() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith('/'): loaders[name] = zf.read(name).decode('utf-8') # Broadcast loaders dict (picklable) to all workers loaders_broadcast = sc.broadcast(loaders) # Define UDF - precompiles on first call, then reuses @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) # Load and process data df = spark.read.parquet("s3://your-bucket/input/") df = df.repartition(200) logger.info(f"Processing {df.count()} rows") result_df = df.withColumn("result", evaluate_rules(col("data"))) result_df.write \ .mode("overwrite") \ .parquet("s3://your-bucket/output/") job.commit() ``` ## Processing structured columns Process data from separate columns rather than JSON: ```python theme={null} from pyspark.sql.types import StringType, DoubleType, BooleanType bc = loaders_broadcast @udf(returnType=StringType()) def evaluate_pricing(customer_tier, years_active, order_subtotal, item_count): import json ZenEvaluator.initialize(bc.value) result = ZenEvaluator.evaluate("pricing", { "customer": { "tier": customer_tier, "yearsActive": int(years_active) if years_active else 0 }, "order": { "subtotal": float(order_subtotal) if order_subtotal else 0, "items": int(item_count) if item_count else 0 } }) return json.dumps(result["result"]) result_df = df.withColumn( "pricing", evaluate_pricing( col("customer_tier"), col("years_active"), col("order_subtotal"), col("item_count") ) ) ``` ## Error handling Return structured results with success/error information: ```python theme={null} from pyspark.sql.types import StructType, StructField, BooleanType, StringType result_schema = StructType([ StructField("success", BooleanType(), False), StructField("result", StringType(), True), StructField("error", StringType(), True) ]) @udf(returnType=result_schema) def evaluate_rules_safe(row_json): import json try: ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return (True, json.dumps(result["result"]), None) except Exception as e: return (False, None, str(e)) result_df = df.withColumn("evaluation", evaluate_rules_safe(col("data"))) # Filter and handle failures success_df = result_df.filter(col("evaluation.success") == True) failed_df = result_df.filter(col("evaluation.success") == False) # Log failures failed_count = failed_df.count() if failed_count > 0: logger.warn(f"{failed_count} rows failed evaluation") failed_df.write.mode("overwrite").parquet("s3://your-bucket/failed/") ``` ## Reading from Glue Data Catalog ```python theme={null} from awsglue.dynamicframe import DynamicFrame # Read from cataloged table dynamic_frame = glueContext.create_dynamic_frame.from_catalog( database="my_database", table_name="my_table" ) df = dynamic_frame.toDF() result_df = df.withColumn("result", evaluate_rules(col("data"))) # Write back to S3 with catalog update glueContext.write_dynamic_frame.from_options( frame=DynamicFrame.fromDF(result_df, glueContext, "result"), connection_type="s3", connection_options={"path": "s3://your-bucket/output/"}, format="parquet" ) ``` ## Job bookmarks Enable job bookmarks to process only new data incrementally: ```python theme={null} args = getResolvedOptions(sys.argv, ['JOB_NAME', 'TempDir']) # Read with bookmark - only processes new data since last run dynamic_frame = glueContext.create_dynamic_frame.from_catalog( database="my_database", table_name="my_table", transformation_ctx="datasource" # Required for bookmarks ) df = dynamic_frame.toDF() # Process result_df = df.withColumn("result", evaluate_rules(col("data"))) # Write with bookmark tracking glueContext.write_dynamic_frame.from_options( frame=DynamicFrame.fromDF(result_df, glueContext, "result"), connection_type="s3", connection_options={"path": "s3://your-bucket/output/"}, format="parquet", transformation_ctx="datasink" # Required for bookmarks ) job.commit() # Commits bookmark state ``` ## Multiple rule files Process with multiple decision files using a single loaders dict. All decisions are extracted from `decisions.zip`: ```python theme={null} # Load all decisions from S3 zip at startup response = s3.get_object(Bucket='your-bucket', Key='decisions.zip') zip_bytes = response['Body'].read() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith('/'): loaders[name] = zf.read(name).decode('utf-8') loaders_broadcast = sc.broadcast(loaders) @udf(returnType=StringType()) def evaluate_pricing(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) @udf(returnType=StringType()) def evaluate_eligibility(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("eligibility", input_data) return json.dumps(result["result"]) result_df = ( df .withColumn("pricing", evaluate_pricing(col("data"))) .withColumn("eligibility", evaluate_eligibility(col("data"))) ) ``` ## Parameterized jobs Pass rule location as job parameter: ```python theme={null} args = getResolvedOptions(sys.argv, [ 'JOB_NAME', 'rules_bucket', 'rules_key', 'input_path', 'output_path' ]) # Load all decisions from parameterized S3 zip location response = s3.get_object(Bucket=args['rules_bucket'], Key=args['rules_key']) zip_bytes = response['Body'].read() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith('/'): loaders[name] = zf.read(name).decode('utf-8') loaders_broadcast = sc.broadcast(loaders) # Read from parameterized input df = spark.read.parquet(args['input_path']) # ... process ... # Write to parameterized output result_df.write.mode("overwrite").parquet(args['output_path']) ``` Run with: ```bash theme={null} aws glue start-job-run \ --job-name my-rules-job \ --arguments '{ "--rules_bucket": "my-bucket", "--rules_key": "decisions.zip", "--input_path": "s3://my-bucket/input/", "--output_path": "s3://my-bucket/output/" }' ``` ## Performance tuning ### Worker configuration | Worker Type | vCPU | Memory | Recommended Partitions | | ----------- | ---- | ------ | ---------------------- | | G.1X | 4 | 16 GB | 2-4 per worker | | G.2X | 8 | 32 GB | 4-8 per worker | | G.4X | 16 | 64 GB | 8-16 per worker | | G.8X | 32 | 128 GB | 16-32 per worker | ### Repartitioning ```python theme={null} # Calculate optimal partitions num_workers = 10 # Set based on your job configuration partitions_per_worker = 4 total_partitions = num_workers * partitions_per_worker df = df.repartition(total_partitions) ``` ### Coalesce for output ```python theme={null} # Reduce partitions before writing to avoid small files result_df.coalesce(10).write.mode("overwrite").parquet("s3://your-bucket/output/") ``` ## Best practices **Precompile on initialize.** The `ZenEvaluator` converts `dict[str, str]` to `dict[str, ZenDecisionContent]` once, then the loader returns precompiled content for maximum performance. **Broadcast loaders dict.** Broadcast `dict[str, str]` (picklable) to all workers. Each worker precompiles once on first use. **Use engine.evaluate directly.** No need to call `create_decision` - the engine's loader handles everything. **Repartition appropriately.** Match partition count to worker count (typically 2-4x the number of DPUs). **Enable job bookmarks.** For incremental processing, use `transformation_ctx` on both reads and writes. **Store rules in S3.** Keep decision files in S3 for easy updates without redeploying the job. The `ZenEvaluator` precompiles JSON strings to `ZenDecisionContent` on first initialization per worker. The engine's loader then returns precompiled content, avoiding repeated JSON parsing. This provides optimal performance for high-throughput processing. # Polars Rules Engine Source: https://docs.gorules.io/developers/integrations/polars High-performance rule evaluation with Python and Polars DataFrames. Process DataFrames efficiently using the ZEN Engine with Polars `map_elements`. ## Installation ```bash theme={null} pip install zen-engine polars ``` ## Singleton evaluator Use a singleton pattern with precompiled `ZenDecisionContent` for optimal performance: ```python theme={null} import zen class ZenEvaluator: """ Singleton for ZenEngine with precompiled ZenDecisionContent. Accepts dict[str, str] (picklable), precompiles to dict[str, ZenDecisionContent], then engine.evaluate() uses the loader to return precompiled content. """ _engine: zen.ZenEngine | None = None _content: dict[str, zen.ZenDecisionContent] = {} _raw: dict[str, str] = {} @classmethod def initialize(cls, loaders: dict[str, str]): """Precompile loaders dict to ZenDecisionContent.""" if cls._raw != loaders: cls._raw = loaders cls._content = {k: zen.ZenDecisionContent(v) for k, v in loaders.items()} cls._engine = None @classmethod def _get_engine(cls) -> zen.ZenEngine: if cls._engine is None: def loader(key: str) -> zen.ZenDecisionContent: return cls._content[key] cls._engine = zen.ZenEngine({"loader": loader}) return cls._engine @classmethod def evaluate(cls, key: str, context: dict) -> dict: """Evaluate a decision by key with given context.""" return cls._get_engine().evaluate(key, context) ``` ## Basic usage ```python theme={null} import polars as pl import json # Load decisions into loaders dict with open('./pricing.json') as f: loaders = {"pricing": f.read()} ZenEvaluator.initialize(loaders) # Sample data df = pl.DataFrame({ "id": [1, 2, 3], "data": [ '{"customer": {"tier": "gold"}, "order": {"subtotal": 150}}', '{"customer": {"tier": "silver"}, "order": {"subtotal": 50}}', '{"customer": {"tier": "bronze"}, "order": {"subtotal": 200}}', ] }) # Process with map_elements (UDF-like) def evaluate_rules(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) output_df = df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) print(output_df) ``` ## Cloud storage Load all decisions from a single zip file at startup for optimal performance. ### AWS S3 ```python theme={null} import boto3 import polars as pl import zen import zipfile import io import json s3 = boto3.client("s3") # Download and extract all decisions at startup obj = s3.get_object(Bucket="my-rules-bucket", Key="decisions.zip") zip_bytes = obj["Body"].read() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") ZenEvaluator.initialize(loaders) def evaluate_rules(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) output_df = df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) ``` ### Azure Blob Storage ```python theme={null} from azure.storage.blob import BlobServiceClient import polars as pl import zen import zipfile import io import os import json blob_service = BlobServiceClient.from_connection_string(os.environ["AZURE_STORAGE_CONNECTION"]) container = blob_service.get_container_client("rules") # Download and extract all decisions at startup blob = container.get_blob_client("decisions.zip") zip_bytes = blob.download_blob().readall() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") ZenEvaluator.initialize(loaders) def evaluate_rules(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) output_df = df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) ``` ### Google Cloud Storage ```python theme={null} from google.cloud import storage import polars as pl import zen import zipfile import io import json client = storage.Client() bucket = client.bucket("my-rules-bucket") # Download and extract all decisions at startup blob = bucket.blob("decisions.zip") zip_bytes = blob.download_as_bytes() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") ZenEvaluator.initialize(loaders) def evaluate_rules(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) output_df = df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) ``` ## Processing structured columns Use `pl.struct` to combine multiple columns for evaluation: ```python theme={null} import polars as pl import json df = pl.DataFrame({ "customer_tier": ["gold", "silver", "bronze"], "years_active": [3, 2, 0], "order_subtotal": [150.0, 50.0, 200.0], "item_count": [5, 2, 10], }) def evaluate_pricing(row: dict) -> str: input_data = { "customer": {"tier": row["customer_tier"], "yearsActive": row["years_active"]}, "order": {"subtotal": row["order_subtotal"], "items": row["item_count"]} } result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) output_df = df.with_columns( pl.struct(["customer_tier", "years_active", "order_subtotal", "item_count"]) .map_elements(evaluate_pricing, return_dtype=pl.Utf8) .alias("result") ) ``` ## Extracting result fields Extract individual fields from results: ```python theme={null} def get_discount(row: dict) -> float: input_data = { "customer": {"tier": row["customer_tier"], "yearsActive": row["years_active"]}, "order": {"subtotal": row["order_subtotal"], "items": row["item_count"]} } result = ZenEvaluator.evaluate("pricing", input_data) return float(result["result"]["discount"]) def get_free_shipping(row: dict) -> bool: input_data = { "customer": {"tier": row["customer_tier"], "yearsActive": row["years_active"]}, "order": {"subtotal": row["order_subtotal"], "items": row["item_count"]} } result = ZenEvaluator.evaluate("pricing", input_data) return bool(result["result"]["freeShipping"]) struct_col = pl.struct(["customer_tier", "years_active", "order_subtotal", "item_count"]) output_df = df.with_columns([ struct_col.map_elements(get_discount, return_dtype=pl.Float64).alias("discount"), struct_col.map_elements(get_free_shipping, return_dtype=pl.Boolean).alias("free_shipping"), ]) ``` ## Error handling Return structured results with success/error information: ```python theme={null} def evaluate_rules_safe(row_json: str) -> dict: try: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return {"success": True, "result": json.dumps(result["result"]), "error": None} except Exception as e: return {"success": False, "result": None, "error": str(e)} output_df = df.with_columns( pl.col("data").map_elements(evaluate_rules_safe, return_dtype=pl.Struct({ "success": pl.Boolean, "result": pl.Utf8, "error": pl.Utf8, })).alias("evaluation") ) # Unnest and filter results = output_df.unnest("evaluation") success_df = results.filter(pl.col("success") == True) failed_df = results.filter(pl.col("success") == False) ``` ## Lazy evaluation Filter data before processing: ```python theme={null} # Lazy scan with predicates filtered_df = ( pl.scan_parquet("input.parquet") .filter(pl.col("status") == "pending") .filter(pl.col("amount") > 100) .collect() ) # Then process filtered data output_df = filtered_df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) ``` ## Batch processing Process large files in batches: ```python theme={null} df = pl.read_parquet("large_input.parquet") batch_size = 10_000 all_batches = [] for i in range(0, df.shape[0], batch_size): batch_df = df.slice(i, batch_size) processed_batch = batch_df.with_columns( pl.col("data").map_elements(evaluate_rules, return_dtype=pl.Utf8).alias("result") ) all_batches.append(processed_batch) output_df = pl.concat(all_batches) output_df.write_parquet("output.parquet") ``` ## Parallel processing For CPU-bound workloads, use multiprocessing with the same pattern: ```python theme={null} from multiprocessing import Pool, cpu_count import zen import json # Global for multiprocessing _mp_engine: zen.ZenEngine | None = None _mp_content: dict[str, zen.ZenDecisionContent] = {} _mp_raw: dict[str, str] = {} def init_worker(loaders: dict[str, str]): """Precompile loaders to ZenDecisionContent on worker init.""" global _mp_engine, _mp_content, _mp_raw _mp_raw = loaders _mp_content = {k: zen.ZenDecisionContent(v) for k, v in loaders.items()} _mp_engine = None def _get_mp_engine() -> zen.ZenEngine: global _mp_engine, _mp_content if _mp_engine is None: def loader(key: str) -> zen.ZenDecisionContent: return _mp_content[key] _mp_engine = zen.ZenEngine({"loader": loader}) return _mp_engine def evaluate_row_worker(row_json): if row_json is None: return None input_data = json.loads(row_json) result = _get_mp_engine().evaluate("pricing", input_data) return json.dumps(result["result"]) # Load rules into loaders dict with open('./pricing.json') as f: loaders = {"pricing": f.read()} df = pl.read_parquet("input.parquet") rows = df["data"].to_list() with Pool(cpu_count(), initializer=init_worker, initargs=(loaders,)) as pool: results = pool.map(evaluate_row_worker, rows) output_df = df.with_columns(pl.Series("result", results)) ``` ## Multiple decisions Evaluate multiple rule sets from a single loaders dict: ```python theme={null} # Load multiple rules with open('./pricing.json') as f: pricing_content = f.read() with open('./eligibility.json') as f: eligibility_content = f.read() loaders = { "pricing": pricing_content, "eligibility": eligibility_content, } ZenEvaluator.initialize(loaders) def evaluate_pricing(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) def evaluate_eligibility(row_json: str) -> str: input_data = json.loads(row_json) result = ZenEvaluator.evaluate("eligibility", input_data) return json.dumps(result["result"]) output_df = df.with_columns([ pl.col("data").map_elements(evaluate_pricing, return_dtype=pl.Utf8).alias("pricing"), pl.col("data").map_elements(evaluate_eligibility, return_dtype=pl.Utf8).alias("eligibility"), ]) ``` ## Best practices **Precompile on initialize.** The `ZenEvaluator` converts `dict[str, str]` to `dict[str, ZenDecisionContent]` once. The loader returns precompiled content for maximum performance. **Use engine.evaluate directly.** No need to call `create_decision` - the engine's loader handles everything. **Use loaders dict.** Store rules as `dict[str, str]` (picklable for multiprocessing), precompile once on init. **Use `map_elements` for UDF-like behavior.** This is Polars' equivalent of Spark UDFs. **Use `pl.struct` for multiple columns.** Combine columns into a struct before applying `map_elements`. **Use lazy evaluation for filtering.** Apply predicates with `scan_parquet` before collecting to reduce data processed. **Use multiprocessing for large datasets.** The GIL limits threading benefits; use process-based parallelism instead. The `ZenEvaluator` precompiles JSON strings to `ZenDecisionContent` on first initialization. The engine's loader returns precompiled content, avoiding repeated JSON parsing. This provides optimal performance for high-throughput processing. # PySpark Rules Engine Source: https://docs.gorules.io/developers/integrations/pyspark Evaluate rules at scale with distributed PySpark DataFrames. Process large datasets with distributed rule evaluation using PySpark UDFs and broadcast variables. ## Installation ```bash theme={null} pip install zen-engine pyspark ``` ## Singleton evaluator Use a singleton pattern with precompiled `ZenDecisionContent` for optimal performance: ```python theme={null} import zen class ZenEvaluator: """ Singleton for ZenEngine with precompiled ZenDecisionContent. Broadcast dict[str, str] (picklable), precompile to dict[str, ZenDecisionContent], then engine.evaluate() uses the loader to return precompiled content. """ _engine: zen.ZenEngine | None = None _content: dict[str, zen.ZenDecisionContent] = {} _raw: dict[str, str] = {} @classmethod def initialize(cls, loaders: dict[str, str]): """Precompile loaders dict to ZenDecisionContent.""" if cls._raw != loaders: cls._raw = loaders cls._content = {k: zen.ZenDecisionContent(v) for k, v in loaders.items()} cls._engine = None @classmethod def _get_engine(cls) -> zen.ZenEngine: if cls._engine is None: def loader(key: str) -> zen.ZenDecisionContent: return cls._content[key] cls._engine = zen.ZenEngine({"loader": loader}) return cls._engine @classmethod def evaluate(cls, key: str, context: dict) -> dict: """Evaluate a decision by key with given context.""" return cls._get_engine().evaluate(key, context) ``` ## Basic usage ```python theme={null} from pyspark.sql import SparkSession from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType import json spark = SparkSession.builder.appName("RulesEvaluation").getOrCreate() # Load decisions into loaders dict and broadcast with open('./pricing.json') as f: loaders = {"pricing": f.read()} loaders_broadcast = spark.sparkContext.broadcast(loaders) # Define UDF with cached ZenDecision @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) # Process data df = spark.read.parquet("s3://bucket/input.parquet") result_df = df.withColumn("result", evaluate_rules(col("data"))) result_df.write.mode("overwrite").parquet("s3://bucket/output.parquet") ``` ## Cloud storage Load all decisions from a single zip file at startup for optimal performance. ### AWS S3 ```python theme={null} import boto3 from pyspark.sql import SparkSession from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType import zipfile import io import json spark = SparkSession.builder.appName("RulesEvaluation").getOrCreate() s3 = boto3.client("s3") # Download and extract all decisions at startup obj = s3.get_object(Bucket="my-rules-bucket", Key="decisions.zip") zip_bytes = obj["Body"].read() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") loaders_broadcast = spark.sparkContext.broadcast(loaders) @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) result_df = df.withColumn("result", evaluate_rules(col("data"))) ``` ### Azure Blob Storage ```python theme={null} from azure.storage.blob import BlobServiceClient from pyspark.sql import SparkSession from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType import zipfile import io import os import json spark = SparkSession.builder.appName("RulesEvaluation").getOrCreate() blob_service = BlobServiceClient.from_connection_string(os.environ["AZURE_STORAGE_CONNECTION"]) container = blob_service.get_container_client("rules") # Download and extract all decisions at startup blob = container.get_blob_client("decisions.zip") zip_bytes = blob.download_blob().readall() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") loaders_broadcast = spark.sparkContext.broadcast(loaders) @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) result_df = df.withColumn("result", evaluate_rules(col("data"))) ``` ### Google Cloud Storage ```python theme={null} from google.cloud import storage from pyspark.sql import SparkSession from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType import zipfile import io import json spark = SparkSession.builder.appName("RulesEvaluation").getOrCreate() client = storage.Client() bucket = client.bucket("my-rules-bucket") # Download and extract all decisions at startup blob = bucket.blob("decisions.zip") zip_bytes = blob.download_as_bytes() loaders = {} with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: for name in zf.namelist(): if not name.endswith("/"): loaders[name] = zf.read(name).decode("utf-8") loaders_broadcast = spark.sparkContext.broadcast(loaders) @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing.json", input_data) return json.dumps(result["result"]) result_df = df.withColumn("result", evaluate_rules(col("data"))) ``` ## Processing structured columns Process data from separate columns: ```python theme={null} from pyspark.sql.functions import col, udf from pyspark.sql.types import StringType, DoubleType, BooleanType bc = loaders_broadcast @udf(returnType=StringType()) def evaluate_pricing(customer_tier, years_active, order_subtotal, item_count): import json ZenEvaluator.initialize(bc.value) result = ZenEvaluator.evaluate("pricing", { "customer": { "tier": customer_tier, "yearsActive": int(years_active) if years_active else 0 }, "order": { "subtotal": float(order_subtotal) if order_subtotal else 0, "items": int(item_count) if item_count else 0 } }) return json.dumps(result["result"]) result_df = df.withColumn( "pricing", evaluate_pricing( col("customer_tier"), col("years_active"), col("order_subtotal"), col("item_count") ) ) ``` ## Extracting result fields Return specific result fields directly: ```python theme={null} @udf(returnType=DoubleType()) def get_discount(customer_tier, years_active, order_subtotal, item_count): ZenEvaluator.initialize(bc.value) result = ZenEvaluator.evaluate("pricing", { "customer": {"tier": customer_tier, "yearsActive": int(years_active or 0)}, "order": {"subtotal": float(order_subtotal or 0), "items": int(item_count or 0)} }) return float(result["result"]["discount"]) @udf(returnType=BooleanType()) def get_free_shipping(customer_tier, years_active, order_subtotal, item_count): ZenEvaluator.initialize(bc.value) result = ZenEvaluator.evaluate("pricing", { "customer": {"tier": customer_tier, "yearsActive": int(years_active or 0)}, "order": {"subtotal": float(order_subtotal or 0), "items": int(item_count or 0)} }) return bool(result["result"]["freeShipping"]) result_df = df.withColumn( "discount", get_discount(col("customer_tier"), col("years_active"), col("order_subtotal"), col("item_count")) ).withColumn( "free_shipping", get_free_shipping(col("customer_tier"), col("years_active"), col("order_subtotal"), col("item_count")) ) ``` ## Error handling Return structured results with success/error information: ```python theme={null} from pyspark.sql.types import StructType, StructField, BooleanType, StringType result_schema = StructType([ StructField("success", BooleanType(), False), StructField("result", StringType(), True), StructField("error", StringType(), True) ]) @udf(returnType=result_schema) def evaluate_rules_safe(row_json): import json try: ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return (True, json.dumps(result["result"]), None) except Exception as e: return (False, None, str(e)) result_df = df.withColumn("evaluation", evaluate_rules_safe(col("data"))) # Filter successful and failed evaluations success_df = result_df.filter(col("evaluation.success") == True) failed_df = result_df.filter(col("evaluation.success") == False) ``` ## Multiple decisions Evaluate multiple rule sets from a single loaders dict: ```python theme={null} # Load multiple rules into loaders dict with open('./pricing.json') as f: pricing_content = f.read() with open('./eligibility.json') as f: eligibility_content = f.read() loaders = { "pricing": pricing_content, "eligibility": eligibility_content, } loaders_broadcast = spark.sparkContext.broadcast(loaders) @udf(returnType=StringType()) def evaluate_pricing(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) @udf(returnType=StringType()) def evaluate_eligibility(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("eligibility", input_data) return json.dumps(result["result"]) result_df = ( df .withColumn("pricing", evaluate_pricing(col("data"))) .withColumn("eligibility", evaluate_eligibility(col("data"))) ) ``` ## Performance tuning ### Repartition for parallelism ```python theme={null} # Repartition for better parallelism (2-4x number of cores) df = df.repartition(200) result_df = df.withColumn("result", evaluate_rules(col("data"))) # Coalesce to reduce partitions for output result_df.coalesce(10).write.parquet("output.parquet") ``` ### Helper function for UDF creation ```python theme={null} def make_evaluate_udf(loaders_broadcast): """Create a UDF that uses broadcast loaders with cached ZenDecision.""" @udf(returnType=StringType()) def evaluate_rules(row_json): import json ZenEvaluator.initialize(loaders_broadcast.value) input_data = json.loads(row_json) result = ZenEvaluator.evaluate("pricing", input_data) return json.dumps(result["result"]) return evaluate_rules # Usage evaluate_rules = make_evaluate_udf(loaders_broadcast) result_df = df.withColumn("result", evaluate_rules(col("data"))) ``` ## Best practices **Precompile on initialize.** The `ZenEvaluator` converts `dict[str, str]` to `dict[str, ZenDecisionContent]` once, then the loader returns precompiled content for maximum performance. **Broadcast loaders dict.** Broadcast `dict[str, str]` (picklable) to all workers. Each worker precompiles once on first use. **Use engine.evaluate directly.** No need to call `create_decision` - the engine's loader handles everything. **Repartition for parallelism.** Match partition count to cluster parallelism (typically 2-4x cores). **Use Parquet for I/O.** Parquet provides efficient columnar storage and predicate pushdown. The `ZenEvaluator` precompiles JSON strings to `ZenDecisionContent` on first initialization per worker. The engine's loader then returns precompiled content, avoiding repeated JSON parsing. This provides optimal performance for high-throughput processing. # JDM Editor Source: https://docs.gorules.io/developers/jdm/jdm-editor Embed the GoRules decision editor in your React application. The JDM Editor is an open-source React component that provides a full-featured visual editor for creating and editing decision models. Embed it in your application to give users the ability to build rules without leaving your product. View source, report issues, and contribute ## Installation ```bash npm theme={null} npm install @gorules/jdm-editor ``` ```bash yarn theme={null} yarn add @gorules/jdm-editor ``` ```bash pnpm theme={null} pnpm add @gorules/jdm-editor ``` ## Quick start The `DecisionGraph` component requires a `JdmConfigProvider` wrapper: ```tsx theme={null} import { useState } from 'react'; import { JdmConfigProvider, DecisionGraph } from '@gorules/jdm-editor'; import '@gorules/jdm-editor/dist/style.css'; function RuleEditor() { const [value, setValue] = useState({ nodes: [], edges: [] }); return ( ); } ``` ## Loading WASM for code extensions The editor uses WebAssembly for syntax highlighting, autocomplete, and expression validation. Load the WASM module at application startup using top-level await: ```tsx theme={null} import * as ZenEngineWasm from '@gorules/zen-engine-wasm'; import wasmUrl from '@gorules/zen-engine-wasm/dist/zen_engine_wasm_bg.wasm?url'; await ZenEngineWasm.default(wasmUrl); ``` WASM requires these headers on your server for `SharedArrayBuffer` support: ``` Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin ``` ## Features The decision graph canvas lets you drag and drop nodes onto the canvas, connect nodes to define data flow, pan and zoom to navigate large graphs, and select and delete nodes. The spreadsheet-like decision table editor supports adding input and output columns, defining conditions with unary operators, reordering rows by drag and drop, and hit policy configuration (first, collect, and per-column collect via `[]` output fields). The expression editor provides syntax highlighting for ZEN expressions, autocomplete for functions and fields, inline validation and error messages, and field path suggestions from input schema. The JavaScript function editor includes Monaco editor with syntax highlighting, TypeScript type checking, built-in library support (dayjs, big.js, zod), and async/await support. ## Configuration ### Read-only mode Display decisions without allowing edits: ```tsx theme={null} ``` ## Simulator integration Add a simulator panel to test decisions directly in the editor. You can evaluate rules via your backend API or entirely in the browser using WASM. Call your backend to evaluate the decision: ```tsx theme={null} import { DecisionGraph, GraphSimulator } from '@gorules/jdm-editor'; import { PlayCircleOutlined } from '@ant-design/icons'; function RuleEditor() { const [graph, setGraph] = useState({ nodes: [], edges: [] }); const [simulation, setSimulation] = useState(); return ( , renderPanel: () => ( setSimulation(undefined)} onRun={async ({ graph, context }) => { const response = await fetch('/api/evaluate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, content: graph }) }); const data = await response.json(); setSimulation({ result: { ...data, snapshot: graph } }); }} /> ), }, ]} /> ); } ``` Evaluate decisions entirely in the browser using the ZEN Engine WASM build: ```tsx theme={null} import { DecisionGraph, GraphSimulator } from '@gorules/jdm-editor'; import { ZenEngine } from '@gorules/zen-engine'; import { PlayCircleOutlined } from '@ant-design/icons'; function RuleEditor() { const [graph, setGraph] = useState({ nodes: [], edges: [] }); const [simulation, setSimulation] = useState(); return ( , renderPanel: () => ( setSimulation(undefined)} onRun={async ({ graph, context }) => { const engine = new ZenEngine(); const result = await engine.evaluate(graph, context, { trace: true }); engine.dispose(); setSimulation({ result: { ...result, snapshot: graph } }); }} /> ), }, ]} /> ); } ``` WASM requires these headers on your server for `SharedArrayBuffer` support: ``` Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin ``` ## Standalone components Use individual editor components outside of the decision graph for custom integrations. ### Code editor The `CodeEditor` component provides standalone expression editing with syntax highlighting and validation: ```tsx theme={null} import { useState } from 'react'; import { JdmConfigProvider, CodeEditor } from '@gorules/jdm-editor'; function ExpressionEditor() { const [expression, setExpression] = useState('customer.age >= 18'); return ( ); } ``` #### Editor types | Type | Use case | | ---------- | -------------------------------------------------- | | `standard` | General expressions that return a value | | `unary` | Boolean conditions for decision table cells | | `template` | String templates with `{expression}` interpolation | ```tsx theme={null} // Standard expression // Unary condition // Template string ``` #### Type hints Pass `variableType` and `expectedVariableType` to enable type checking and improved autocomplete. Types use the `VariableTypeJson` format: ```tsx theme={null} import type { VariableTypeJson } from '@gorules/jdm-editor'; const variableType: VariableTypeJson = { Object: { customer: { Object: { name: 'String', age: 'Number', tier: { Enum: [undefined, ['bronze', 'silver', 'gold']] } } }, order: { Object: { amount: 'Number', items: { Array: 'Any' } } } } }; ``` Available type variants: | Type | Description | | ---------------------------------------------- | ------------------------------- | | `'Any'` | Any value | | `'Null'` | Null value | | `'Bool'` | Boolean | | `'String'` | String | | `'Number'` | Number | | `{ Const: string }` | Constant string value | | `{ Enum: [label, values] }` | Enumeration with optional label | | `{ Array: VariableTypeJson }` | Array of a specific type | | `{ Object: Record }` | Object with typed fields | ### Decision table The `DecisionTable` component renders a standalone spreadsheet-style table editor: ```tsx theme={null} import { useState } from 'react'; import { JdmConfigProvider, DecisionTable } from '@gorules/jdm-editor'; import type { DecisionTableType } from '@gorules/jdm-editor'; function TableEditor() { const [table, setTable] = useState({ hitPolicy: 'first', inputs: [], outputs: [], rules: [], }); return ( ); } ``` #### Schema-driven autocomplete Pass `inputsSchema` and `outputsSchema` to provide field suggestions in the expression editor: ```tsx theme={null} import type { SchemaSelectProps } from '@gorules/jdm-editor'; const inputsSchema: SchemaSelectProps[] = [ { field: 'customer.tier', name: 'Customer Tier' }, { field: 'customer.region', name: 'Region' }, { field: 'order.amount', name: 'Order Amount' }, { field: 'order.quantity', name: 'Quantity' }, ]; const outputsSchema: SchemaSelectProps[] = [ { field: 'discount', name: 'Discount' }, { field: 'shippingFee', name: 'Shipping Fee' }, ]; ``` #### Permission levels Control editing capabilities with the `permission` prop: | Permission | Description | | ------------- | ------------------------------------------------ | | `edit:full` | Full editing capabilities (default) | | `edit:rules` | Edit rule values only, cannot add/remove columns | | `edit:values` | Edit cell values only, cannot modify structure | ```tsx theme={null} ``` ## Schema validation Validate JDM files before loading them into the editor using the exported Zod schema: ```tsx theme={null} import { decisionModelSchema } from '@gorules/jdm-editor/dist/schema'; async function handleFileUpload(file: File) { const content = await file.text(); const result = decisionModelSchema.safeParse(JSON.parse(content)); if (!result.success) { console.error(result.error); alert('Invalid decision file'); return; } setGraph(result.data); } ``` ## TypeScript support The editor is fully typed. Import types for your integration: ```tsx theme={null} import type { DecisionGraphType, DecisionNode, DecisionEdge, DecisionTableType, SchemaSelectProps, } from '@gorules/jdm-editor'; const decision: DecisionGraphType = { nodes: [], edges: [], }; ``` ## Extracting JDM output The editor produces JDM (JSON Decision Model) format that the ZEN Engine can execute: ```tsx theme={null} function SaveButton({ value }: { value: DecisionGraphType }) { const handleSave = async () => { // value is already in JDM format await fetch('/api/decisions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(value), }); }; return ; } ``` ## Standalone editor For non-React applications, use the [Standalone Editor](/developers/jdm/standalone-editor) - a self-hosted Docker image with the full visual editor. # Node types Source: https://docs.gorules.io/developers/jdm/node-types Reference for all JDM node types and their content schemas. Every node in a JDM file has a `type` that determines its behavior and content structure. ## Available node types | Type | Description | | ------------------- | ----------------------------------- | | `inputNode` | Entry point for decision input | | `outputNode` | Exit point returning the result | | `decisionTableNode` | Spreadsheet-style conditional logic | | `expressionNode` | ZEN expression transformations | | `functionNode` | Custom JavaScript logic | | `switchNode` | Conditional branching | | `decisionNode` | Sub-decision reference | ## Common node properties All nodes share these properties: | Property | Type | Description | | ---------- | ------ | --------------------------- | | `id` | string | Unique identifier | | `type` | string | Node type | | `name` | string | Display name | | `position` | object | Canvas position `{ x, y }` | | `content` | object | Type-specific configuration | ## Input node The entry point for every decision. Receives the data you pass to `evaluate()`. ```json theme={null} { "id": "input", "type": "inputNode", "name": "Request", "position": { "x": 0, "y": 100 }, "content": { "schema": "" } } ``` | Property | Type | Description | | -------- | ------ | ----------------------------------------- | | `schema` | string | Optional JSON Schema for input validation | ## Output node The exit point that returns the decision result. ```json theme={null} { "id": "output", "type": "outputNode", "name": "Response", "position": { "x": 600, "y": 100 }, "content": { "schema": "" } } ``` | Property | Type | Description | | -------- | ------ | ------------------------------------------ | | `schema` | string | Optional JSON Schema for output validation | ## Decision table node Spreadsheet-style conditional logic with inputs, outputs, and rules. ```json theme={null} { "type": "decisionTableNode", "content": { "hitPolicy": "first", "inputs": [ { "id": "i1", "name": "Customer Tier", "field": "customer.tier" }, { "id": "i2", "name": "Order Total", "field": "order.total" } ], "outputs": [ { "id": "o1", "name": "Discount", "field": "discount" } ], "rules": [ { "_id": "r1", "i1": "\"gold\"", "i2": ">= 100", "o1": "0.15" }, { "_id": "r2", "i1": "", "i2": "", "o1": "0" } ], "passThrough": true, "inputField": null, "outputPath": null, "executionMode": "single" } } ``` ### Content properties | Property | Type | Default | Description | | --------------- | ------------------------ | ---------- | -------------------------------------- | | `hitPolicy` | `"first"` \| `"collect"` | `"first"` | Return first match or all matches | | `inputs` | array | | Input column definitions | | `outputs` | array | | Output column definitions | | `rules` | array | | Rule rows (keyed by column id) | | `passThrough` | boolean | `true` | Include input data in output | | `inputField` | string \| null | `null` | Array field to iterate (for loop mode) | | `outputPath` | string \| null | `null` | Path to store output | | `executionMode` | `"single"` \| `"loop"` | `"single"` | Process once or iterate array | ### Input column | Property | Type | Description | | -------- | ------ | ---------------------------------------- | | `id` | string | Unique identifier (used in rules) | | `name` | string | Display label | | `field` | string | Path to input value (enables unary mode) | ### Output column | Property | Type | Description | | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | Unique identifier (used in rules) | | `name` | string | Display label | | `field` | string | Output field path; end it with `[]` to collect the column across all matching rows while the rest of the table stays first-hit | | `type` | string | Optional declared type for cell validation | ### Rules format Each rule is an object with `_id` and entries keyed by column id: ```json theme={null} { "_id": "rule-1", "i1": ">= 100", "i2": "\"gold\"", "o1": "0.15" } ``` Leave a cell empty (`""`) to match any value. ## Expression node Transform data using ZEN expressions. ```json theme={null} { "type": "expressionNode", "content": { "expressions": [ { "id": "e1", "key": "subtotal", "value": "sum(map(items, #.price * #.qty))" }, { "id": "e2", "key": "tax", "value": "$.subtotal * 0.08" }, { "id": "e3", "key": "total", "value": "$.subtotal + $.tax" } ], "passThrough": true, "inputField": null, "outputPath": null, "executionMode": "single" } } ``` ### Content properties | Property | Type | Default | Description | | --------------- | ---------------------- | ---------- | -------------------------------------- | | `expressions` | array | | Key-value expression pairs | | `passThrough` | boolean | `true` | Include input data in output | | `inputField` | string \| null | `null` | Array field to iterate (for loop mode) | | `outputPath` | string \| null | `null` | Path to store output | | `executionMode` | `"single"` \| `"loop"` | `"single"` | Process once or iterate array | ### Expression entry | Property | Type | Description | | -------- | ------ | ----------------- | | `id` | string | Unique identifier | | `key` | string | Output field name | | `value` | string | ZEN expression | Use `$` to reference values calculated earlier in the same node. ## Function node Custom JavaScript for complex logic. ```json theme={null} { "type": "functionNode", "content": { "source": "/** @type {Handler} */\nexport const handler = async (input) => {\n return { loyaltyPoints: Math.floor(input.total * 1.5) };\n}" } } ``` | Property | Type | Description | | -------- | ------ | ------------------------ | | `source` | string | JavaScript function code | Function nodes support: * ES6+ JavaScript syntax * `async/await` for asynchronous operations * Built-in modules: `dayjs`, `big.js`, `zod`, `http`, `zen` ## Switch node Route data through different paths based on conditions. ```json theme={null} { "type": "switchNode", "content": { "hitPolicy": "first", "statements": [ { "id": "s1", "condition": "customer.tier == 'enterprise'", "isDefault": false }, { "id": "s2", "condition": "customer.tier == 'business'", "isDefault": false }, { "id": "s3", "condition": "", "isDefault": true } ] } } ``` ### Content properties | Property | Type | Default | Description | | ------------ | ------------------------ | --------- | ---------------------------------- | | `hitPolicy` | `"first"` \| `"collect"` | `"first"` | Execute first match or all matches | | `statements` | array | | Branch conditions | ### Statement | Property | Type | Description | | ----------- | ------- | --------------------------------------------- | | `id` | string | Unique identifier (used as edge sourceHandle) | | `condition` | string | ZEN expression evaluating to boolean | | `isDefault` | boolean | True for the fallback branch | ### Connecting switch outputs Each statement id becomes a source handle for edges: ```json theme={null} { "edges": [ { "id": "e1", "sourceId": "switch-1", "targetId": "enterprise-node", "sourceHandle": "s1", "type": "edge" }, { "id": "e2", "sourceId": "switch-1", "targetId": "business-node", "sourceHandle": "s2", "type": "edge" }, { "id": "e3", "sourceId": "switch-1", "targetId": "default-node", "sourceHandle": "s3", "type": "edge" } ] } ``` ## Decision node Reference another decision (sub-decision). ```json theme={null} { "type": "decisionNode", "content": { "key": "pricing/calculate-discount", "passThrough": true, "inputField": null, "outputPath": null, "executionMode": "single" } } ``` | Property | Type | Default | Description | | --------------- | ---------------------- | ---------- | -------------------------------------- | | `key` | string | | Path to the referenced decision | | `passThrough` | boolean | `true` | Include input data in output | | `inputField` | string \| null | `null` | Array field to iterate (for loop mode) | | `outputPath` | string \| null | `null` | Path to store output | | `executionMode` | `"single"` \| `"loop"` | `"single"` | Process once or iterate array | ## Edge schema Edges connect nodes in the graph: ```json theme={null} { "id": "edge-1", "sourceId": "node-a", "targetId": "node-b", "sourceHandle": null, "type": "edge" } ``` | Property | Type | Description | | -------------- | -------------- | -------------------------------- | | `id` | string | Unique identifier | | `sourceId` | string | Source node id | | `targetId` | string | Target node id | | `sourceHandle` | string \| null | Output handle (for switch nodes) | | `type` | `"edge"` | Always "edge" | # Standalone Editor Source: https://docs.gorules.io/developers/jdm/standalone-editor Self-host the GoRules visual editor with built-in simulator using Docker. The Standalone Editor is a self-hosted web application for creating and testing JDM (JSON Decision Model) files. Run it locally or deploy it to your infrastructure. View source, report issues, and contribute ## Quick start Run the editor with Docker: ```bash theme={null} docker run -p 3000:3000 gorules/editor ``` Open `http://localhost:3000` in your browser. ## When to use **Use the Standalone Editor when:** * You want to self-host the editor * You're prototyping rules locally * You need an air-gapped environment * You're building rules for embedded SDK use **Use the BRMS when:** * Multiple people edit rules * You need version history and audit trails * You want environment management * You need access control ## Features * **Decision graph canvas** - Drag and drop nodes, connect them visually * **Decision tables** - Spreadsheet-style conditional logic * **Expression nodes** - ZEN language calculations * **Function nodes** - Custom JavaScript code * **Switch nodes** - Conditional branching * **Built-in simulator** - Test rules with sample data and view execution traces * **Export** - Download decisions as JDM files ## Web playground A hosted version is available at [editor.gorules.io](https://editor.gorules.io) - no installation required. ## Using exported files Load your exported JDM file with any GoRules SDK: ## Limitations The Standalone Editor doesn't include: * Cloud storage * Version control * Team collaboration * Environment management * Access control * Audit logging For these features, use the [GoRules BRMS](/brms/setup/projects). # JDM Standard Source: https://docs.gorules.io/developers/jdm/standard The JSON Decision Model format used by GoRules for portable, version-controllable business rules. JDM (JSON Decision Model) is the file format used by GoRules to represent decision graphs. It's a human-readable JSON structure that captures nodes, edges, and configuration in a portable format. ## Why JDM? | Benefit | Description | | ------------------------ | -------------------------------------------------- | | **Portable** | Works across all GoRules SDKs and tools | | **Version controllable** | Store in Git alongside your code | | **Human readable** | Review and diff changes easily | | **Extensible** | Add custom metadata without breaking compatibility | ## File structure A JDM file contains two sections: ```json theme={null} { "nodes": [...], "edges": [...] } ``` ### Nodes Each node represents a processing step in the decision: ```json theme={null} { "id": "node-1", "type": "decisionTableNode", "name": "Customer Discount", "position": { "x": 200, "y": 100 }, "content": { // Node-specific configuration } } ``` ### Edges Edges define connections between nodes: ```json theme={null} { "id": "edge-1", "sourceId": "input-node", "targetId": "decision-table-node", "sourceHandle": "output" } ``` ### Additional fields The engine ignores unknown top-level fields, so tools can attach their own metadata without breaking evaluation. Only `nodes` and `edges` are part of the format. ## Loading JDM files Load and evaluate JDM files using any GoRules SDK: ### Validating structure The `@gorules/jdm-editor` package exports a Zod schema for validating JDM files: ```typescript theme={null} import { decisionModelSchema } from '@gorules/jdm-editor/dist/schema'; const content = JSON.parse(await file.text()); const result = decisionModelSchema.safeParse(content); if (!result.success) { console.error('Invalid JDM:', result.error.issues); } ``` ## Version control best practices ### File organization ### Meaningful commits ```bash theme={null} git commit -m "Add enterprise tier discount (20% for orders > $10k)" ``` ### Review diffs JDM's JSON format makes diffs readable: ```diff theme={null} "rules": [ { "_id": "rule-1", "i1": "\"gold\"", - "o1": "0.15" + "o1": "0.20" } ] ``` ## Exporting from BRMS The GoRules BRMS exports decisions in JDM format: 1. Open the decision in BRMS 2. Click **Export** in the toolbar 3. Choose **JSON Decision Model (.json)** 4. Save to your repository ## Schema reference The complete JSON Schema for JDM files is available in the [zen repository](https://github.com/gorules/zen). # MCP integration Source: https://docs.gorules.io/developers/mcp Connect AI-powered editors to GoRules BRMS through the Model Context Protocol. The [GoRules CLI](/developers/cli) exposes an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that lets AI assistants interact with your GoRules BRMS projects. When connected, AI tools can read, modify, simulate, and test your decision graphs directly. ## How it works The GoRules CLI starts a lightweight MCP server that connects to your browser session via WebSocket. When an AI assistant sends a tool call, the CLI forwards it to the GoRules web app running in your browser, which executes the operation against your in-memory decision graphs and returns the result. ```mermaid theme={null} graph LR A[AI Assistant] <-->|MCP| B[GoRules CLI] B <-->|WebSocket| C[GoRules Browser Editor] ``` This architecture keeps all execution local - decision graphs are evaluated in your browser, and no data leaves your machine. ## Setup ### 1. Start the MCP server ```bash theme={null} npx @gorules/cli mcp start ``` The CLI starts on port `41919` by default and generates an 8-character token displayed in the terminal. ### 2. Connect in the browser 1. Open GoRules in your browser and navigate to a branch. 2. Click the **MCP Server** button in the editor toolbar. 3. Enter the 8-character token from the terminal. 4. The status dot turns green when connected. ### 3. Configure your AI assistant ```bash theme={null} claude mcp add gorules --transport http http://localhost:41919/mcp ``` Add to `.cursor/mcp.json` in your project root: ```json theme={null} { "mcpServers": { "gorules": { "url": "http://localhost:41919/mcp" } } } ``` Add to your Windsurf MCP config: ```json theme={null} { "mcpServers": { "gorules": { "url": "http://localhost:41919/mcp" } } } ``` Any MCP client that supports Streamable HTTP can connect: ```json theme={null} { "mcpServers": { "gorules": { "url": "http://localhost:41919/mcp" } } } ``` ## Available tools Tools are dynamically registered by the GoRules browser editor when it connects. The exact tools depend on your project, but generally cover graph introspection, graph mutation, decision table operations, simulation, test management, and file operations. The MCP server instructs AI assistants to call `get_current_context` first. This provides the assistant with full context about available tools and the connected project. ## Connection status The **MCP Server** button in the GoRules toolbar shows the connection state: | Status | Indicator | Meaning | | ------------ | ----------------- | ----------------------------------------------- | | Connected | Green pulsing dot | MCP server is active and linked to your session | | Connecting | Amber dot | WebSocket connection is being established | | Disconnected | Gray dot | No active connection | Click the **MCP Server** button again to disconnect. ## Configuration | Option | Default | Description | | -------------- | ----------- | ------------------------------------------- | | `--port`, `-p` | `41919` | Port the bridge listens on | | `--host`, `-h` | `localhost` | Host the bridge binds to | | `--url`, `-u` | - | GoRules server URL (used with `--open`) | | `--open` | `false` | Open the GoRules editor in browser on start | ## Security * Connections are authenticated with an 8-character token generated per session. * All tool execution happens in your browser - decision graphs are never sent to the CLI or AI assistant directly. * The WebSocket connection runs on `localhost` only. * Only one browser tab can be active on the MCP bridge at a time. ## Troubleshooting **"Browser not connected"** - The GoRules editor hasn't connected yet. Open your project and click **MCP Server**. **"Tool execution timed out (60s)"** - The browser tab may be in the background or unresponsive. Bring the GoRules editor tab to the foreground. **"Browser disconnected"** - The WebSocket connection was lost. Reconnect from the GoRules editor. # Architecture Source: https://docs.gorules.io/developers/overview/architecture Understand GoRules system architecture, components, and deployment patterns. GoRules architecture separates **rule management** from **rule execution**. This separation enables business users to author and test rules centrally while developers deploy execution engines across any environment. ## Architecture overview Enterprise GoRules deployments consist of two main components: | Component | Purpose | | ------------------- | --------------------------------------------------------------------- | | **GoRules BRMS** | Management layer - author, test, version, simulate, and publish rules | | **Execution layer** | Runtime layer - evaluate rules in your applications via Agent or SDK | GoRules deployment architecture ## Management: GoRules BRMS The BRMS (Business Rules Management System) is where you create and manage rules. It provides a web interface for business users and APIs for developer automation. ### Capabilities * **Authoring** - Visual editor for decision tables, graphs, expressions, functions, and custom apps * **Testing** - Built-in simulator to validate rules before deployment * **Versioning** - Git-like branching, merging, and history for all rules * **Publishing** - Create release artifacts and deploy to any environment * **Collaboration** - Multi-user editing with role-based access control and conflict resolution ### Components The BRMS bundles three components in a single Docker image, distributed via [Docker Hub](https://hub.docker.com/r/gorules/brms): | Component | Purpose | | -------------- | ------------------------------------------------------------ | | **BRMS UI** | Web interface for rule authoring and collaboration | | **BRMS API** | RESTful interface for integration and automation | | **ZEN Engine** | High-performance rules evaluation for testing and simulation | ### Infrastructure requirements | Requirement | Specification | | ------------ | ------------------------------------------- | | **Database** | PostgreSQL 12+ (required for all BRMS data) | | **Runtime** | Docker (Linux x86\_64) | | **Memory** | 1GB minimum per instance | | **CPU** | 0.5 vCPU minimum | | **Network** | Access to portal.gorules.io for licensing | The BRMS is stateless - all data (projects, decisions, users, audit logs) lives in PostgreSQL. This enables horizontal scaling by adding instances behind a load balancer. ## Execution After publishing rules from the BRMS, you have two options for executing them in your target environments. Both the [Agent](https://github.com/gorules/agent-public) and the [ZEN Engine SDK](https://github.com/gorules/zen) are open source. Rules reach the execution layer either pushed directly from BRMS or through your own pipeline - see the CI/CD integration guides for [GitHub Actions](/developers/cicd/github-actions), [GitLab CI](/developers/cicd/gitlab-ci), and [Azure DevOps](/developers/cicd/azure-devops). Whether you use GoRules Cloud or self-hosted BRMS, the execution layer (Agent or SDK) always runs inside your own infrastructure. Your data never leaves your environment during rule evaluation. ### GoRules Agent The Agent is an open-source headless rules engine that serves decisions over REST API. Built entirely in Rust, it delivers maximum performance with minimal resource usage. Distributed via [Docker Hub](https://hub.docker.com/r/gorules/agent). **Best for:** Language-agnostic integration, centralized execution, automatic updates without code changes. ```mermaid theme={null} flowchart LR brms[BRMS] -- Publish Release --> storage[Object Storage] agent[Agent] -- Poll & Reload --> storage service[Service] -- Evaluate --> agent ``` **How deployment works:** 1. **Publish** - In BRMS, merge changes to main branch and create a release. The release artifact deploys to object storage (S3, Azure Blob, GCS). 2. **Detect** - Agent polls object storage on synchronized intervals. When the remote etag differs from local, Agent knows a new version is available. 3. **Reload** - Agent pulls the new package into memory and compiles rules at runtime. The switch happens atomically with zero downtime. This pattern supports multiple environments (DEV, UAT, PROD) with independent Agents polling from different storage paths or buckets. | Requirement | Specification | | ------------------ | ------------------------------------- | | **Runtime** | Docker (Linux x86\_64) | | **Memory** | 512MB minimum | | **CPU** | 0.25 vCPU minimum | | **Storage access** | S3, Azure Blob, GCS, or bundled files | ### ZEN Engine SDK Embed the ZEN Engine directly in your application code for in-process evaluation. ```mermaid theme={null} flowchart TB subgraph app[Your Application] sdk[ZEN Engine SDK] end ``` **Best for:** Sub-millisecond latency, offline capability, tight integration. **Supported languages:** [Rust](/developers/sdks/rust), [Go](/developers/sdks/go), [Python](/developers/sdks/python), [Node.js](/developers/sdks/nodejs), [Java](/developers/sdks/java), [Kotlin](/developers/sdks/kotlin), [Swift](/developers/sdks/swift) ```mermaid theme={null} flowchart LR brms[BRMS] -- Export --> git[Git Repository] cicd[CI/CD] -- Bundle & Deploy --> app[Application + SDK + Rules] ``` **How deployment works:** 1. **Export** - Use BRMS API to export decision files to your Git repository 2. **Build** - Your CI/CD pipeline packages rules with the application 3. **Execute** - SDK loads rules from bundled files or fetches from storage at startup This pattern gives you full control over when rules deploy and enables offline execution. ## Multi-environment setup A typical enterprise deployment separates environments while sharing a single BRMS instance. Multi-environment GoRules architecture on Azure Each environment (DEV, UAT, PROD) runs its own Agent instances with IAM-scoped access to environment-specific storage paths. CI/CD integration is optional - you can publish directly from BRMS or integrate with your existing release pipelines: [GitHub Actions](/developers/cicd/github-actions), [GitLab CI](/developers/cicd/gitlab-ci), or [Azure DevOps](/developers/cicd/azure-devops). ## Scaling Both BRMS and Agent support horizontal and vertical scaling. Because management and execution are decoupled, you can scale each layer independently based on your needs. **Horizontal scaling** - Add more instances behind a load balancer. Both BRMS and Agent are stateless, so you can scale out without coordination. **Vertical scaling** - Increase memory and CPU to handle larger rule sets or higher throughput. The Agent's Rust implementation provides linear scaling with available resources. ### Performance comparison | Metric | Agent | SDK | | ---------- | ------------------------- | --------------------------- | | Latency | 10-20ms (same VNET) | \< 1ms | | Throughput | 1-10K req/s (single core) | 10-100K req/s (single core) | | Hot-reload | Yes (zero downtime) | Depends on implementation | Performance varies based on rule complexity, extremely complex models with thousands of rules can run slower. Decision tables and expression nodes achieve the highest throughput, while decisions using Function nodes run slower due to JavaScript runtime overhead. See [Performance benchmarks](/developers/overview/performance) for detailed SDK measurements. ## High availability For production deployments: 1. **Multiple BRMS instances** - Run 2+ replicas behind a load balancer 2. **Database replication** - Use managed PostgreSQL with read replicas 3. **Storage redundancy** - Enable multi-AZ and versioning on object storage 4. **Multiple Agent instances** - Run at least 2 Agent replicas in production with health checks for failover See [Disaster recovery](/developers/overview/disaster-recovery) for more details. # BRE and BRMS Source: https://docs.gorules.io/developers/overview/bre-vs-brms Understand how the ZEN Engine (BRE) and GoRules BRMS work together. GoRules consists of two main parts: the **BRMS** (Business Rules Management System) for managing rules, and the **ZEN Engine** (BRE) for executing them. The **BRMS** is the management platform where you author, test, version, and publish rules. It includes the ZEN Engine internally for testing and direct API evaluation. The **ZEN Engine** is the high-performance execution engine. It runs inside the BRMS, and also powers the Agent and SDKs that execute rules in your environments. ## GoRules BRMS The BRMS is the recommended starting point for most organizations. It provides everything you need to manage business rules at scale. | Capability | Description | | ------------------- | ------------------------------------------------------ | | **Visual editor** | Drag-and-drop interface for building decision graphs | | **Collaboration** | Multiple users, projects, and access controls | | **Version control** | Track changes, compare versions, roll back | | **Testing** | Built-in simulator to validate rules before publishing | | **Releases** | Publish rules to different environments | | **Audit logs** | Track who changed what and when | | **API access** | Evaluate rules directly or publish to Agents/SDKs | ### When to use BRMS * Business users need to create or modify rules * You need version control and audit trails * Multiple teams collaborate on rules * You want a managed workflow for publishing rules ## ZEN Engine The ZEN Engine is the open-source rules execution engine that powers all of GoRules. It's written in Rust for maximum performance. | Capability | Description | | ------------------- | -------------------------------------------------------------- | | **Sub-millisecond** | Evaluates most decisions in under 1ms | | **Cross-platform** | Native SDKs for Node.js, Python, Go, Rust, Java, Kotlin, Swift | | **Embeddable** | Runs in-process with your application | | **Open-source** | Available on [GitHub](https://github.com/gorules/zen) | ### Standalone usage You can use the ZEN Engine without the BRMS if you only need rule execution and will manage rule files yourself. | Component | Purpose | | ------------------------------------------------------ | ---------------------------------- | | [ZEN Engine](https://github.com/gorules/zen) | Core execution library | | [JDM Editor](https://github.com/gorules/jdm-editor) | React component for building rules | | [Standalone Editor](https://github.com/gorules/editor) | Desktop app for editing JDM files | **When to use standalone:** * Building a product with embedded rules * You have your own storage and versioning * Developers manage all rules (no business user access needed) * Maximum control over the rules lifecycle When using the ZEN Engine standalone, you're responsible for storing, versioning, and distributing JDM files to your applications. ## Using them together Organizations often use both BRMS and ZEN Engine together when they need maximum performance: 1. **Author in BRMS** - Business users create and test rules in the visual editor 2. **Publish releases** - Rules are published to object storage (S3, Azure, GCS) 3. **Execute via SDK** - ZEN Engine runs embedded in your application With the embedded SDK, rule evaluation is sub-millisecond because there's no network overhead - no REST API calls, no serialization. The engine runs in-process with your application. ```mermaid theme={null} flowchart LR brms["BRMS
(author & publish)"] --> storage["Object Storage"] storage --> app subgraph app["Your Application"] sdk["ZEN Engine SDK"] end ``` ## Choosing your approach | Scenario | Recommendation | | ----------------------------------------- | ------------------------------ | | Business users edit rules | BRMS | | Need audit trails and version control | BRMS | | Team collaboration | BRMS | | Building a product with embedded rules | ZEN Engine standalone | | Developers manage all rules | Either (BRMS adds convenience) | | Maximum performance, minimal dependencies | ZEN Engine standalone | Most organizations start with the BRMS and use Agents or SDKs for production execution. The standalone ZEN Engine is best suited for product teams embedding rules into their own software. # Disaster recovery Source: https://docs.gorules.io/developers/overview/disaster-recovery Strategies for backup, failover, and recovery of GoRules deployments. GoRules separates rule management (BRMS) from rule execution (Agent/SDK). This decoupled architecture simplifies disaster recovery - your production workloads continue even if the BRMS is temporarily unavailable. ## Recovery priorities | Component | Impact if unavailable | Priority | | ---------------------------------- | ------------------------------------------------------------- | -------- | | **Execution layer** (Agent or SDK) | Applications can't evaluate rules | High | | **Object Storage** | Agents can't load new rules (existing rules remain in memory) | High | | **BRMS** | Can't author or publish new rules | Medium | | **PostgreSQL** | BRMS unavailable | Medium | ## Management layer (BRMS) Because the management layer is decoupled from execution, the BRMS does not require maximum availability. A straightforward setup is sufficient: **Horizontal scaling** - Run at least 2 BRMS replicas behind a load balancer for failover. **Database HA** - Use managed PostgreSQL with high availability enabled: * AWS Aurora PostgreSQL (Multi-AZ) * Azure Database for PostgreSQL Flexible Server (HA mode) * Google Cloud SQL (High availability) **Backups** - Configure automated backups with point-in-time recovery. This is standard in modern managed database services and protects against data corruption or accidental deletion. If BRMS becomes unavailable, rule execution continues uninterrupted. Users cannot author or publish new rules until BRMS is restored, but all existing rules remain operational. ## Execution layer The execution layer requires high availability since it handles live traffic. ### Agent deployment When using the Agent, the only external dependency is object storage - which cloud providers design for high availability across multiple availability zones. **Run at least 2 Agent replicas** in production with health checks. Spread replicas across availability zones when possible. **Agent resilience** - Once rules are loaded into memory, the Agent continues serving requests even if object storage becomes temporarily unavailable. Rules are not ejected automatically on storage failure. ### SDK deployment When using the SDK with bundled rules, there are no external dependencies - your application is self-contained. High availability depends entirely on how you deploy your service. Run with horizontal scaling and standard HA patterns for your platform. ## Cross-region availability For extreme availability requirements, you can deploy across multiple regions without running BRMS in every region. ```mermaid theme={null} flowchart TB subgraph primary[Primary Region] brms[BRMS] -- Publish --> storageA[Object Storage] agentA[Agent] -- Poll --> storageA end subgraph secondary[Secondary Region] storageB[Object Storage] agentB[Agent] -- Poll --> storageB end storageA -.-> storageB ``` **How it works:** 1. BRMS runs in a single region and publishes releases to object storage 2. Object storage replicates to a secondary region (using native cloud replication) 3. Agents in each region poll their local storage bucket 4. When BRMS publishes a release, both regions receive the same rules automatically This approach provides regional failover for rule execution while keeping the management layer simple. ## Recovery procedures ### Agent failure Traffic automatically routes to healthy replicas via your load balancer. Replace failed instances and investigate root cause from logs. ### Storage failure Agents continue serving with rules already in memory. If using cross-region replication, update Agent configuration to use the replica bucket. Restore primary storage when possible. ### BRMS failure Rule execution continues unaffected. Redeploy BRMS containers and verify database connectivity. Users can resume authoring once restored. ## Recovery objectives | Metric | Typical target | | ---------------------------------- | ----------------------------------- | | **RTO** (Recovery Time Objective) | Near-zero with multi-replica Agents | | **RPO** (Recovery Point Objective) | 0 with storage replication | With replicated storage and multi-replica Agents, most failures are handled automatically without downtime. # Open source Source: https://docs.gorules.io/developers/overview/open-source Transparency you can trust - audit the code, run it yourself, and own your business logic Our engine is fully open source. Audit the code. Run it yourself. Fork it if you need to. We believe critical business logic shouldn't be a black box - you deserve to see exactly how your decisions are made. High-performance rules engine written in Rust with bindings for Node.js, Python, Go, and more. Processes millions of evaluations per second. React component for visual rule editing. Embed the same decision graph editor used in the BRMS directly into your applications. ## Why open source matters Inspect every line that runs your rules. No hidden logic, no surprises. Leave anytime. Your rules are portable JSON files you own. Built with feedback from teams like yours. Shape the roadmap. # Performance Source: https://docs.gorules.io/developers/overview/performance Benchmark results and performance characteristics across ZEN Engine language bindings. The ZEN Engine is written in Rust and achieves exceptional throughput across all language bindings. On a MacBook Pro M3 single-core, the engine processes an average of **91,000 evaluations per second** across 74 real-world decision benchmarks. Continuously updated cross-language benchmarks - every scenario measured in µs/op against the Rust baseline, with a snapshot timeline and per-binding comparison. ## Benchmark highlights | Scenario | Throughput | | -------------------------- | ---------- | | Realtime fraud detection | 191K req/s | | Dynamic FX rate pricing | 184K req/s | | Insurance agent commission | 148K req/s | | Loan approval | 45K req/s | | Company analysis (complex) | 4K req/s | Performance varies based on decision structure. Pure decision tables and expression nodes achieve 150K+ evaluations per second. Decisions using [Function nodes](/learn/authoring/function-nodes) run slower (3-50K req/s) due to JavaScript runtime overhead. ## Language binding performance All bindings share the same Rust core. The binding layer adds minimal overhead for most languages. | Language | Binding | Overhead | Notes | | ------------------- | ---------------------------------------------- | -------- | -------------------------------------------- | | Rust | Native | None | Baseline performance | | Node.js | [NAPI-RS](https://napi.rs) | Low | Near-native, minimal serialization overhead | | Python | [PyO3](https://pyo3.rs) | Low | Efficient type conversions, async support | | Go | Custom C + CGO | Low | CGO boundary overhead, native evaluation | | WebAssembly | WASM + NAPI | Low | Browser and edge deployments | | Swift | [UniFFI](https://mozilla.github.io/uniffi-rs/) | Low | Idiomatic Swift APIs | | Kotlin/Java/Android | JNA (UniFFI) | High | JVM boundary and object marshalling overhead | ## Performance optimization tips **Pre-compile decisions.** Use `ZenDecisionContent` to parse and compile decisions once, then reuse them for multiple evaluations. This is available in Node.js, Python, and Go SDKs. **Reuse engine instances.** Create a single engine at startup rather than instantiating per-request. Engine creation has initialization costs. **Batch evaluations when possible.** If you're evaluating the same decision with many inputs, batch them to amortize any per-call overhead. **Profile your specific decisions.** Performance varies significantly based on decision complexity. A simple lookup table runs 50x faster than a complex multi-stage graph with custom functions. ## Benchmark methodology The [benchmark suite](https://gorules.github.io/zen/bench) runs the same real-world decision fixtures through the Rust core and each language binding, and publishes results per scenario in µs/op - Rust is the baseline floor, and the snapshot timeline shows how results evolve across engine releases. The headline figures on this page come from a MacBook Pro with Apple M3 chip using single-core execution. Each scenario represents a real-world business rule pattern: * **Decision tables only** (100K+ req/s): Pure table lookups and expressions * **Mixed graphs** (50-100K req/s): Multiple nodes, branching logic * **Function-heavy** (3-50K req/s): Decisions using Function nodes with JavaScript execution The benchmark measures pure evaluation time excluding I/O, network latency, and decision loading. Production throughput depends on your infrastructure, decision structure, and data serialization overhead. The slowest benchmarks (Company analysis, Insurance breakdown, AML) use Function nodes extensively. If you need maximum throughput, prefer decision tables and expression nodes over custom JavaScript functions. # AWS ECS Fargate Source: https://docs.gorules.io/developers/platform-guides/aws-ecs Deploy GoRules BRMS on AWS using ECS with Fargate and Aurora Serverless. Deploy GoRules BRMS on AWS Elastic Container Service (ECS) with Fargate for serverless container management. This guide covers a scalable, cost-effective, and easily maintainable infrastructure. ## Architecture overview GoRules deployment architecture on AWS **Components:** * **GoRules BRMS** - Containerized application * **AWS ECS Fargate** - Serverless compute engine for containers * **Aurora Serverless v2** - Auto-scaling relational database Terraform modules are coming soon. ## Database - Setting up Aurora Serverless ### 1. Initiate database creation 1. Log in to your AWS Management Console 2. Navigate to the RDS (Relational Database Service) dashboard 3. Click **Create database** ### 2. Choose engine options 1. Select "Standard Create" 2. Select "Aurora (PostgreSQL-Compatible)" as the engine type 3. Under templates, choose "Production" or "Dev/Test" depending on the environment Aurora engine options ### 3. Set up database credentials 1. Set "DB cluster identifier" (e.g., `gorules-aurora-pg-cluster`) 2. Enter a master username (e.g., `gorules_admin`) 3. Manually create a strong password 4. Store this password securely; you'll need it later as an environment variable Database credentials ### 4. Configure database instance 1. For "Capacity settings", choose "Serverless" 2. Set the minimum and maximum Aurora Capacity Units (ACUs) based on your expected workload Database instance configuration ### 5. Configure network and security 1. Choose the appropriate VPC for your ECS Fargate deployment 2. Select or create a new DB Subnet Group 3. For "Public access", choose "No" unless your architecture requires it 4. Create or select a VPC security group that allows inbound traffic on the Aurora port (default 5432 for PostgreSQL) from your ECS tasks ### 6. Additional configuration 1. Set the initial database name (e.g., `gorules_db`) 2. Configure backup retention period as needed 3. Enable encryption at rest (recommended) 4. Enable deletion protection (recommended) ### 7. Finalize and create 1. Review all settings 2. Click **Create database** at the bottom of the page ### 8. Wait for completion 1. The creation process may take several minutes 2. Monitor progress in the RDS dashboard ### 9. Retrieve connection information Once the database is available, note down the endpoint. This endpoint will be used in your application's connection string. Store the master password as an environment variable or in AWS Secrets Manager. Never commit sensitive information to version control. ## Container runner - Setting up ECS Fargate ### 1. Create ECS cluster ECS cluster list 1. Navigate to the ECS dashboard in the AWS Management Console 2. Click **Create cluster** 3. Choose "AWS Fargate" under infrastructure 4. Set Cluster name (e.g., `gorules-cluster`) 5. Optionally enable CloudWatch Container Insights and add tags 6. Click **Create** Create ECS cluster ### 2. Create task definition Create task definition 1. In the ECS dashboard, go to "Task Definitions" and click **Create new Task Definition** 2. Set Task Definition Name (e.g., `gorules-brms-task`) 3. Select "Fargate" as the launch type compatibility 4. Under architecture, choose Linux X86\_64 5. Set Task memory and CPU (for dev environment 0.5 CPU and 1GB RAM is enough) 6. Click **Add container** and configure: * Container name (e.g., `gorules-container`) * Image URI (from your ECR repository or other registry) * Port mappings (Port 80) * Environment variables (including database connection string) 7. Add any additional containers if required (e.g., for logging or monitoring) 8. Click **Add** to add the container to the task definition 9. Review and click **Create** to create the task definition Task definition create ### 3. Create ECS service Create ECS service 1. Go to your ECS Cluster 2. Click **Create** under the Services tab 3. Configure the service: * Capacity provider: FARGATE * Task Definition: Select the task definition you created * Service name (e.g., `gorules-service`) * Number of tasks: Set based on your requirements 4. Configure networking if required 5. Configure load balancing (Application Load Balancer) 6. Set Auto Scaling if needed (optional): * Configure service auto scaling based on CPU utilization or other metrics 7. Review and click **Create Service** Service details Service details continued Load balancer configuration Load balancer configuration continued Monitor your ECS service in the AWS Console to ensure tasks are running correctly. Set up CloudWatch logs and configure alarms for notifications. ## Notes * To make your service accessible, edit the security group and allow inbound traffic from all IPv4 (or configure appropriately for your needs) * To obtain `DB_SSL_CA`, visit the [AWS RDS SSL documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesDownload), find the certificate for your region, and use the [GoRules Base64 Certificate tool](https://gorules.io/tools/base64-certificate) to convert it # Azure Container Apps Source: https://docs.gorules.io/developers/platform-guides/azure-container-apps Deploy GoRules BRMS on Azure Container Apps with PostgreSQL flexible servers. Deploy GoRules BRMS on Azure Container Apps for a serverless container experience. This guide covers a scalable, cost-effective, and easily maintainable infrastructure. ## Architecture overview GoRules deployment architecture on Azure **Components:** * **GoRules BRMS** - Containerized application * **Azure Container Apps** - Serverless compute engine for containers * **Azure Database for PostgreSQL flexible servers** - Managed PostgreSQL relational database Terraform modules are coming soon. ## Database - Setting up Azure Database for PostgreSQL ### 1. Initiate database creation 1. Log in to the [Azure Portal](https://portal.azure.com) with your Microsoft account credentials 2. Click **Create a resource** 3. Search for "Azure Database for PostgreSQL Flexible Server" 4. Click **Create** ### 2. Project details 1. Select your Azure subscription 2. Choose or create a new resource group ### 3. Server details 1. Enter a unique name for your server in **Server name** 2. Select the Azure region where you will host BRMS 3. Select PostgreSQL version 16 4. Select workload type (e.g., Production Small/Medium) 5. Customize to 2 vCores and 8GB RAM if needed 6. Enable high availability for production setups ### 4. Authentication 1. Select **PostgreSQL authentication only** 2. Set an admin username and password 3. **Important:** Remember the USERNAME and PASSWORD ### 5. Networking tab 1. Choose your connectivity method 2. Set up firewall rules if needed Your database should not be publicly accessible in production environments. ### 6. Security tab 1. Select **Service-managed key** ### 7. Review and create 1. Review your settings 2. Click **Create** to deploy your PostgreSQL server ### 8. Create database and get configuration 1. After deployment, go to your resource list 2. Open the newly created server 3. Go to **Overview** and copy the **Server Name** on the right side 4. **Important:** Remember the SERVER NAME / HOST 5. From the side menu, select **Databases** and click **Add** 6. Enter a name (e.g., `gorules-dev`) and click **Save** 7. **Important:** Remember the DATABASE name 8. From the side menu, select **Networking** and download the SSL Certificate 9. Use the [GoRules Base64 Certificate tool](https://gorules.io/tools/base64-certificate) to convert the certificate to base64 Never commit sensitive information like database passwords to version control. Always use environment variables or a secure secrets management solution. ## Container runner - Setting up Container Apps ### 1. Create Container Apps 1. Click **Create a resource** 2. Search for "Container Apps" 3. Select and click **Create** ### 2. Basics tab 1. Choose your subscription 2. Choose or create a resource group 3. Enter a container app name 4. Set deployment source to **Container image** ### 3. Container Apps environment 1. Select region (same as database) 2. For a simple setup, leave the managed environment 3. For complex setups, configure your environment manually ### 4. Container tab and finalization 1. Select **Use quickstart image** (we will configure the proper image later) 2. Skip bindings and tags 3. Go to **Review and Create** 4. Review your settings and click **Create** to deploy your container app ### 5. Configuring Container Apps Once deployed, open the newly created Container App: 1. From the left menu, go to **Overview** and copy the URL 2. **Important:** Remember the URL 3. Open the **Containers** menu and click **Edit and Deploy** 4. In the **Container** tab, Container Images section, select the image and click **Edit** #### Properties tab | Field | Value | | -------------- | ------------------------------ | | Name | Your container app name | | Image source | Docker Hub or other registries | | Image type | Public | | Registry login | docker.io | | Image and tag | gorules/brms:latest | | CPU cores | 1 | | Memory | 2Gi | For production, use a specific image version instead of `latest`. #### Environment variables tab | Variable | Description | | ------------------------ | --------------------------------------------------- | | DB\_HOST | Server name from step 8.4 | | DB\_USER | Admin username from step 4.2 | | DB\_PASSWORD | Admin password (should come from vault) | | DB\_NAME | Database name from step 8.6 | | DB\_REJECT\_UNAUTHORIZED | `true` | | DB\_SSL\_CA | Base64 encoded certificate from step 8.9 | | LICENSE\_KEY | From [portal.gorules.io](https://portal.gorules.io) | | APP\_NAME | Your app name (e.g., "Company DEV") | | APP\_URL | URL from step 5.2 | | COOKIE\_SECRET | Secure secret (should come from vault) | #### Health probes tab Enable all 3 probes (Liveness, Readiness, Startup) with these values: | Setting | Value | | -------------- | ----------- | | Path | /api/health | | Initial delay | 5 | | Other settings | Default | 1. Click **Save** and close the side drawer #### Scale tab 1. Set min replicas: 1 (or 2 for production) 2. Set max replicas: 2 (or higher for production) 3. Click **Create** ### 6. Finalize 1. Monitor status in the **Revisions and Replicas** menu 2. Upon successful deployment, open the **Application URL** link in the Overview menu # Docker Compose Source: https://docs.gorules.io/developers/platform-guides/docker-compose Deploy GoRules BRMS with Docker Compose for development and small-scale production. Docker Compose provides the simplest way to run GoRules BRMS with a PostgreSQL database. ## Prerequisites * Docker installed * Docker Compose v2+ ## Quick start Create a `docker-compose.yml` file: ```yaml theme={null} version: '3.8' services: brms: image: gorules/brms ports: - '9080:80' depends_on: - postgres environment: DB_HOST: postgres DB_PORT: 5432 DB_USER: gorules DB_PASSWORD: your-secure-password DB_NAME: gorules DB_SSL_DISABLED: true LICENSE_KEY: your-license-key postgres: image: postgres:15 environment: POSTGRES_USER: gorules POSTGRES_PASSWORD: your-secure-password POSTGRES_DB: gorules volumes: - postgres_data:/var/lib/postgresql/data ports: - '5432:5432' volumes: postgres_data: ``` Start the services: ```bash theme={null} docker-compose up -d ``` Access BRMS at `http://localhost:9080`. ## Scaling For multiple BRMS instances: ```yaml theme={null} services: brms: image: gorules/brms deploy: replicas: 3 # ... rest of config ``` Use with a load balancer or Docker Swarm for production scaling. ## Backup Backup the PostgreSQL volume: ```bash theme={null} # Stop services docker-compose stop # Backup volume docker run --rm \ -v gorules_postgres_data:/data \ -v $(pwd):/backup \ alpine tar czf /backup/postgres-backup.tar.gz -C /data . # Restart services docker-compose start ``` ## Upgrade Update to the latest version: ```bash theme={null} # Pull latest image docker-compose pull # Recreate with new image docker-compose up -d ``` The BRMS automatically runs database migrations on startup. ## Troubleshooting ### Container won't start Check logs: ```bash theme={null} docker-compose logs brms ``` Common issues: * Invalid `LICENSE_KEY` * Database connection failed * Missing required environment variables ### Database connection errors Verify PostgreSQL is healthy: ```bash theme={null} docker-compose exec postgres pg_isready -U gorules ``` ### Reset everything Remove all data and start fresh: ```bash theme={null} docker-compose down -v docker-compose up -d ``` This deletes all data including the PostgreSQL volume. # Kubernetes Source: https://docs.gorules.io/developers/platform-guides/kubernetes Deploy GoRules BRMS on Kubernetes using Helm. Deploy on Kubernetes in a few simple steps. ## Requirements Install [Helm](https://helm.sh/) on your computer and connect to your cluster. | Component | Requirement | | ---------- | --------------------------------------------------- | | Helm | 3.8+ | | Kubernetes | 1.2x+ | | Nodes | Linux OS, x86\_64 architecture | | Resources | Minimum 1GB RAM, 0.5 vCPU | | Database | PostgreSQL | | License | From [portal.gorules.io](https://portal.gorules.io) | ## Configure 1. Download `values.yaml` from [Artifact Hub](https://artifacthub.io/packages/helm/gorules/gorules-brms?modal=values) 2. Rename the file to `my-values.yaml` 3. Replace variables such as `appUrl` and `licenseKey` with your values 4. Fill in the PostgreSQL connection details ## Installation After configuring `my-values.yaml`, install GoRules BRMS: ```bash theme={null} helm install my-gorules-brms oci://registry-1.docker.io/gorulescharts/gorules-brms -f my-values.yaml ``` Install a specific version by adding `--version 1.6.0` before `-f my-values.yaml`. For production environments, set up ingress on a proper URL. ## Test installation Forward the port for testing: ```bash theme={null} kubectl port-forward services/my-gorules-brms 4200:80 ``` BRMS will be available at `http://localhost:4200`. ## Upgrade Upgrade to a new version: ```bash theme={null} helm upgrade my-gorules-brms oci://registry-1.docker.io/gorulescharts/gorules-brms --version x.x.x -f my-values.yaml ``` Replace `--version x.x.x` with the actual version number, such as `--version 1.6.0`. ## FAQ ### Multi-architecture node pools If you're running multi-architecture node pools, force Linux OS and x86\_64 architecture by modifying `nodeSelector` in `my-values.yaml`: ```yaml theme={null} nodeSelector: kubernetes.io/arch: amd64 kubernetes.io/os: linux ``` Then run `helm upgrade` to apply the changes. # Android Rules Engine Source: https://docs.gorules.io/developers/sdks/android Integrate GoRules into your Android application. Install the ZEN Engine and evaluate your first decision in Android. ## Installation ```kotlin theme={null} dependencies { implementation("io.gorules:zen-engine-kotlin-android:2.0.0") } ``` ## Basic usage ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEngine import io.gorules.zen_engine.kotlin_android.JsonBuffer import kotlinx.coroutines.runBlocking class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) runBlocking { val ruleJson = assets.open("rules/pricing.json").readBytes() ZenEngine(null, null).use { engine -> val decision = engine.createDecision(JsonBuffer(ruleJson)) val input = JsonBuffer(""" { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } """) val response = decision.evaluate(input, null) Log.d("ZenEngine", response.result.toString()) // => {"discount":0.15,"freeShipping":true} } } } } ``` ## Loader `ZenEngine` accepts an optional `ZenLoader` that serves decisions by key. Use `Static`, `Filesystem`, or `Zip` for common backends, or `Callback` for custom loading logic. ### Static Register decisions in memory. Use this for rules bundled in your app's assets: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEngine import io.gorules.zen_engine.kotlin_android.ZenLoader import io.gorules.zen_engine.kotlin_android.JsonBuffer fun createEngine(context: Context): ZenEngine { val pricing = context.assets.open("rules/pricing.json").readBytes() val loader = ZenLoader.Static(mapOf("pricing.json" to JsonBuffer(pricing))) return ZenEngine(loader, null) } ``` ### File system Load decisions from files under a root directory, such as internal storage. Keys resolve to paths relative to the root: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEngine import io.gorules.zen_engine.kotlin_android.ZenLoader import java.io.File fun createEngine(context: Context): ZenEngine { val loader = ZenLoader.Filesystem(File(context.filesDir, "rules").absolutePath) return ZenEngine(loader, null) } ``` ### Zip archive Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release once and hand the bytes to the engine: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEngine import io.gorules.zen_engine.kotlin_android.ZenLoader import okhttp3.OkHttpClient import okhttp3.Request import java.io.File val client = OkHttpClient() fun createEngine(context: Context): ZenEngine { val cacheFile = File(context.cacheDir, "decisions.zip") val zipBytes = if (cacheFile.exists()) { cacheFile.readBytes() } else { val request = Request.Builder() .url("https://api.example.com/decisions.zip") .build() val bytes = client.newCall(request).execute().body!!.bytes() cacheFile.parentFile?.mkdirs() cacheFile.writeBytes(bytes) bytes } return ZenEngine(ZenLoader.Zip(zipBytes), null) } ``` ### Custom loader For any other backend, implement `ZenDecisionLoaderCallback` and wrap it in `ZenLoader.Callback`: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEngine import io.gorules.zen_engine.kotlin_android.ZenLoader import io.gorules.zen_engine.kotlin_android.ZenDecisionLoaderCallback import io.gorules.zen_engine.kotlin_android.JsonBuffer import com.google.firebase.remoteconfig.FirebaseRemoteConfig val remoteConfig = FirebaseRemoteConfig.getInstance() fun createEngine(): ZenEngine { val callback = object : ZenDecisionLoaderCallback { override suspend fun load(key: String): JsonBuffer? { val json = remoteConfig.getString("decision_$key") return if (json.isEmpty()) null else JsonBuffer(json) } } return ZenEngine(ZenLoader.Callback(callback), null) } ``` Returning `null` from the callback reports the key as not found. ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```kotlin theme={null} val results = engine.evaluateBatch( listOf( ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 100 }""")), ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 250 }""")) ), null ) for (result in results) { if (result.success) { println(result.data?.result) } else { println("Evaluation failed: ${result.error}") } } ``` ## Coroutines Evaluation functions are `suspend` functions, integrating natively with Kotlin coroutines: ```kotlin theme={null} import kotlinx.coroutines.* lifecycleScope.launch { val results = inputs.map { input -> async(Dispatchers.Default) { decision.evaluate(JsonBuffer(input), null) } }.awaitAll() results.forEach { Log.d("ZenEngine", it.result.toString()) } } ``` ## Error handling ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenException try { val response = decision.evaluate(input, null) Log.d("ZenEngine", response.result.toString()) } catch (e: ZenException) { Log.e("ZenEngine", "Evaluation failed: ${e.message}") } ``` ## Tracing Enable tracing to inspect decision execution: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.ZenEvaluateOptions val options = ZenEvaluateOptions(trace = true, maxDepth = null) val response = decision.evaluate(input, options) Log.d("ZenEngine", "Trace: ${response.trace}") Log.d("ZenEngine", "Performance: ${response.performance}") ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```kotlin theme={null} import io.gorules.zen_engine.kotlin_android.evaluateExpression import io.gorules.zen_engine.kotlin_android.evaluateUnaryExpression import io.gorules.zen_engine.kotlin_android.JsonBuffer // Standard expressions val result = evaluateExpression("a + b", JsonBuffer("""{ "a": 5, "b": 3 }""")) // => 8 val total = evaluateExpression("sum(items)", JsonBuffer("""{ "items": [1, 2, 3, 4] }""")) // => 10 // Unary expressions (comparison against $) val isValid = evaluateUnaryExpression(">= 5", JsonBuffer("""{ "$": 10 }""")) // => true val inList = evaluateUnaryExpression("'US', 'CA', 'MX'", JsonBuffer("""{ "$": "US" }""")) // => true ``` ## Performance note The Android bindings use JNA (Java Native Access) for interoperability with the native Rust engine. This introduces some overhead compared to native Rust or direct bindings. We plan to revisit this when the FFM (Foreign Function & Memory) API becomes more widely adopted. ## Best practices **Use `.use {}` for resource management.** `ZenEngine` implements `AutoCloseable` to release native resources. ```kotlin theme={null} ZenEngine(null, null).use { engine -> // use engine } ``` **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Cache decisions persistently.** Use internal storage or SharedPreferences to cache downloaded decisions for offline use. **Evaluate on background threads.** Use `Dispatchers.Default` or `Dispatchers.IO` to avoid blocking the main thread. **Bundle fallback decisions.** Include decisions in assets as fallback for first launch or network failures. # C# Rules Engine Source: https://docs.gorules.io/developers/sdks/csharp Integrate GoRules into your .NET application. Install the ZEN Engine and evaluate your first decision in C#. ## Installation ```bash theme={null} dotnet add package GoRules.ZenEngine ``` ## Basic usage ```csharp theme={null} using GoRules.ZenEngine; var ruleJson = File.ReadAllBytes("rules/pricing.json"); using var engine = new ZenEngine(loader: null, customNode: null); var decision = engine.CreateDecision(new JsonBuffer(ruleJson)); var input = new JsonBuffer(""" { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } """); var response = await decision.Evaluate(input, null); Console.WriteLine(response.Result); // => {"discount":0.15,"freeShipping":true} decision.Dispose(); ``` ## Loader The `loader` argument accepts a `ZenLoader` that resolves decisions by key. Use `ZenLoader.Callback` to load from any storage backend, or a configuration variant (`Static`, `Filesystem`, `Zip`) to pre-load and pre-compile all decisions when you create the engine. ### Loader configurations Prefer a configuration when your decisions are known up front. The engine compiles them once, so evaluations skip loading and parsing entirely. ```csharp theme={null} using GoRules.ZenEngine; // Static: decisions provided as an in-memory map using var staticEngine = new ZenEngine(loader: new ZenLoader.Static(new Dictionary { ["pricing.json"] = new JsonBuffer(File.ReadAllBytes("rules/pricing.json")) })); // Filesystem: decisions resolved relative to a directory using var fsEngine = new ZenEngine(loader: new ZenLoader.Filesystem("rules")); // Zip: decisions extracted from a zip archive using var zipEngine = new ZenEngine(loader: new ZenLoader.Zip(File.ReadAllBytes("decisions.zip"))); var response = await fsEngine.Evaluate("pricing.json", new JsonBuffer("{}"), null); Console.WriteLine(response.Result); ``` ### File system Implement the `ZenDecisionLoaderCallback` interface with a `Task Load(string key)` method and wrap it in `ZenLoader.Callback`. Return `null` when the decision does not exist. Use `ConcurrentDictionary` to cache decisions for optimal performance. ```csharp theme={null} using GoRules.ZenEngine; using System.Collections.Concurrent; var cache = new ConcurrentDictionary(); using var engine = new ZenEngine(loader: new ZenLoader.Callback(new FileLoader(cache)), customNode: null); var response = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null); Console.WriteLine(response.Result); class FileLoader(ConcurrentDictionary cache) : ZenDecisionLoaderCallback { public Task Load(string key) { var bytes = cache.GetOrAdd(key, k => File.ReadAllBytes(Path.Combine("rules", k))); return Task.FromResult(new JsonBuffer(bytes)); } } ``` ### AWS S3 ```csharp theme={null} using GoRules.ZenEngine; using Amazon.S3; using Amazon.S3.Model; var s3 = new AmazonS3Client(Amazon.RegionEndpoint.USEast1); // Download all decisions as a zip archive at startup var response = await s3.GetObjectAsync(new GetObjectRequest { BucketName = "my-rules-bucket", Key = "decisions.zip" }); using var ms = new MemoryStream(); await response.ResponseStream.CopyToAsync(ms); // ZenLoader.Zip extracts and pre-compiles every decision in the archive using var engine = new ZenEngine(loader: new ZenLoader.Zip(ms.ToArray()), customNode: null); var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null); Console.WriteLine(result.Result); ``` ### Azure Blob Storage ```csharp theme={null} using GoRules.ZenEngine; using Azure.Storage.Blobs; var blobService = new BlobServiceClient(Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION")); var blob = blobService.GetBlobContainerClient("rules").GetBlobClient("decisions.zip"); // Download all decisions as a zip archive at startup var download = await blob.DownloadContentAsync(); // ZenLoader.Zip extracts and pre-compiles every decision in the archive using var engine = new ZenEngine(loader: new ZenLoader.Zip(download.Value.Content.ToArray()), customNode: null); var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null); Console.WriteLine(result.Result); ``` ### Google Cloud Storage ```csharp theme={null} using GoRules.ZenEngine; using Google.Cloud.Storage.V1; var storage = StorageClient.Create(); // Download all decisions as a zip archive at startup using var zipStream = new MemoryStream(); await storage.DownloadObjectAsync("my-rules-bucket", "decisions.zip", zipStream); // ZenLoader.Zip extracts and pre-compiles every decision in the archive using var engine = new ZenEngine(loader: new ZenLoader.Zip(zipStream.ToArray()), customNode: null); var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null); Console.WriteLine(result.Result); ``` ## Async evaluation Evaluation methods return `Task` for native async/await integration: ```csharp theme={null} var tasks = inputs.Select(input => decision.Evaluate(new JsonBuffer(input), null) ).ToArray(); var results = await Task.WhenAll(tasks); foreach (var result in results) { Console.WriteLine(result.Result); } ``` ## Batch evaluation Use `EvaluateBatch` to evaluate many contexts in a single call. Each request pairs a decision key with a context, and each result reports its own success or error: ```csharp theme={null} using GoRules.ZenEngine; var requests = new List { new ZenBatchRequest("pricing.json", new JsonBuffer("""{ "order": { "subtotal": 150 } }""")), new ZenBatchRequest("pricing.json", new JsonBuffer("""{ "order": { "subtotal": 40 } }""")) }; var results = await engine.EvaluateBatch(requests, null); foreach (var result in results) { if (result.Success) { Console.WriteLine(result.Data!.Result); } else { Console.Error.WriteLine($"Evaluation failed: {result.Error}"); } } ``` ## Error handling ```csharp theme={null} using GoRules.ZenEngine; try { var response = await decision.Evaluate(input, null); Console.WriteLine(response.Result); } catch (ZenException.EvaluationException e) { Console.Error.WriteLine($"Evaluation failed: {e.Message}"); } catch (ZenException e) { Console.Error.WriteLine($"Engine error: {e.Message}"); } ``` ## Tracing Enable tracing to inspect decision execution: ```csharp theme={null} using GoRules.ZenEngine; var options = new ZenEvaluateOptions(MaxDepth: null, Trace: true); var response = await decision.Evaluate(input, options); if (response.Trace != null) { foreach (var (nodeId, trace) in response.Trace) { Console.WriteLine($"[{trace.Order}] {trace.Name}: {trace.Output}"); } } Console.WriteLine(response.Performance); // Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```csharp theme={null} using GoRules.ZenEngine; // Standard expressions var result = ZenUniffiMethods.EvaluateExpression( "a + b", new JsonBuffer("""{ "a": 5, "b": 3 }""") ); // => 8 var total = ZenUniffiMethods.EvaluateExpression( "sum(items)", new JsonBuffer("""{ "items": [1, 2, 3, 4] }""") ); // => 10 // Unary expressions (comparison against $) var isValid = ZenUniffiMethods.EvaluateUnaryExpression( ">= 5", new JsonBuffer("""{ "$": 10 }""") ); // => true var inList = ZenUniffiMethods.EvaluateUnaryExpression( "'US', 'CA', 'MX'", new JsonBuffer("""{ "$": "US" }""") ); // => true // Compiled expression (reusable, better performance) using var expr = ZenExpression.Compile("a + b * 2"); var output = expr.Evaluate(new JsonBuffer("""{ "a": 1, "b": 10 }""")); Console.WriteLine(output); // => 21 ``` ## Custom nodes Extend the engine with custom logic by implementing `ZenCustomNodeCallback`: ```csharp theme={null} using GoRules.ZenEngine; using System.Text.Json; // Register the custom node handler when creating the engine using var engine = new ZenEngine(loader: new ZenLoader.Callback(new FileLoader()), customNode: new SumCustomNode()); class SumCustomNode : ZenCustomNodeCallback { public Task Handle(ZenEngineHandlerRequest request) { var input = JsonSerializer.Deserialize(request.input.ToString()); var sum = input.EnumerateObject() .Where(p => p.Value.ValueKind == JsonValueKind.Number) .Sum(p => p.Value.GetDouble()); return Task.FromResult(new ZenEngineHandlerResponse( output: new JsonBuffer(JsonSerializer.Serialize(new { sum })), traceData: null )); } } class FileLoader : ZenDecisionLoaderCallback { public Task Load(string key) => Task.FromResult(File.Exists(key) ? new JsonBuffer(File.ReadAllBytes(key)) : null); } ``` ## Performance note The C# bindings use UniFFI with P/Invoke for interoperability with the native Rust engine. This introduces some overhead compared to native Rust. Native libraries are bundled for Windows (x64), macOS (x64/ARM), and Linux (x64/ARM). ## Best practices **Use `using` for resource management.** `ZenEngine`, `ZenDecision`, and `ZenExpression` implement `IDisposable` to release native resources. ```csharp theme={null} using var engine = new ZenEngine(loader: null, customNode: null); // engine is automatically disposed at end of scope ``` **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching with `ConcurrentDictionary`. **Use `Task.WhenAll` for parallel evaluation.** Evaluate multiple decisions concurrently with async/await. # Go Rules Engine Source: https://docs.gorules.io/developers/sdks/go Integrate GoRules into your Go application. Install the ZEN Engine and evaluate your first decision in Go. ## Installation ```bash theme={null} go get github.com/gorules/zen-go/v2 ``` **Upgrading from v0.x?** Version 2 changes the import path to `github.com/gorules/zen-go/v2`, and callback loaders now require the `zen.Loader` wrapper: `EngineConfig{Loader: zen.Loader(myFunc)}`. Loader configurations (`StaticLoader`, `FilesystemLoader`, `ZipLoader`) and batch evaluation are new in v2. ## Basic usage ```go theme={null} package main import ( "fmt" "os" zen "github.com/gorules/zen-go/v2" ) func main() { content, _ := os.ReadFile("./pricing-rules.json") engine := zen.NewEngine(zen.EngineConfig{}) defer engine.Dispose() decision, _ := engine.CreateDecision(content) defer decision.Dispose() response, _ := decision.Evaluate(map[string]any{ "customer": map[string]any{"tier": "gold", "yearsActive": 3}, "order": map[string]any{"subtotal": 150, "items": 5}, }) fmt.Println(string(response.Result)) // => {"discount":0.15,"freeShipping":true} } ``` ## Loaders Loaders let the engine resolve decisions by key - `engine.Evaluate("pricing.json", ...)` - instead of you reading files by hand. Set `EngineConfig.Loader` to a built-in loader config (`StaticLoader`, `FilesystemLoader`, or `ZipLoader`) or a custom callback. ### Static The static loader serves decisions from an in-memory map. Use it when your rules ship with the application or arrive as one payload: ```go theme={null} package main import ( "encoding/json" "fmt" "os" zen "github.com/gorules/zen-go/v2" ) func main() { pricing, _ := os.ReadFile("./pricing-rules.json") engine := zen.NewEngine(zen.EngineConfig{ Loader: zen.StaticLoader{ Content: map[string]json.RawMessage{ "pricing.json": pricing, }, }, }) defer engine.Dispose() response, _ := engine.Evaluate("pricing.json", map[string]any{"amount": 100}) fmt.Println(string(response.Result)) } ``` ### File system The filesystem loader resolves keys against a root directory - `pricing.json` maps to `./rules/pricing.json`: ```go theme={null} engine := zen.NewEngine(zen.EngineConfig{ Loader: zen.FilesystemLoader{Path: "./rules"}, }) defer engine.Dispose() response, _ := engine.Evaluate("pricing.json", map[string]any{"amount": 100}) ``` ### Zip archive The zip loader unpacks an archive in memory; every `.json` entry becomes a decision keyed by its path in the archive. This pairs naturally with [release ZIPs](/brms/deploy/releases) downloaded from the BRMS or object storage: ```go theme={null} package main import ( "context" "fmt" "io" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" zen "github.com/gorules/zen-go/v2" ) func main() { cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1")) s3Client := s3.NewFromConfig(cfg) result, _ := s3Client.GetObject(context.TODO(), &s3.GetObjectInput{ Bucket: aws.String("my-rules-bucket"), Key: aws.String("release-1.0.0.zip"), }) defer result.Body.Close() zipBytes, _ := io.ReadAll(result.Body) engine := zen.NewEngine(zen.EngineConfig{ Loader: zen.ZipLoader{Bytes: zipBytes}, }) defer engine.Dispose() response, _ := engine.Evaluate("pricing.json", map[string]any{"amount": 100}) fmt.Println(string(response.Result)) } ``` ### Custom loader For any other backend - a database, a remote API, per-tenant storage - pass a callback. Use `sync.Map` to cache at the source: ```go theme={null} package main import ( "fmt" "os" "path/filepath" "sync" zen "github.com/gorules/zen-go/v2" ) var cache sync.Map func loader(key string) ([]byte, error) { if data, ok := cache.Load(key); ok { return data.([]byte), nil } content, err := os.ReadFile(filepath.Join("./rules", key)) if err != nil { return nil, err } cache.Store(key, content) return content, nil } func main() { engine := zen.NewEngine(zen.EngineConfig{Loader: zen.Loader(loader)}) defer engine.Dispose() response, _ := engine.Evaluate("pricing.json", map[string]any{"amount": 100}) fmt.Println(string(response.Result)) } ``` ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```go theme={null} results, err := engine.EvaluateBatch([]zen.EvaluateBatchRequest{ {Key: "pricing.json", Context: map[string]any{"amount": 100}}, {Key: "pricing.json", Context: map[string]any{"amount": 250}}, {Key: "eligibility.json", Context: map[string]any{"age": 30}}, }) if err != nil { log.Fatal(err) } for _, result := range results { if result.Success { fmt.Println(string(result.Data.Result)) } else { fmt.Println("Evaluation failed:", string(result.Error)) } } ``` ## Error handling ```go theme={null} response, err := decision.Evaluate(input) if err != nil { log.Printf("Evaluation failed: %v", err) return err } fmt.Println(string(response.Result)) ``` ## Tracing Enable tracing to inspect decision execution: ```go theme={null} response, err := decision.EvaluateWithOpts(input, zen.EvaluationOptions{ Trace: true, }) if err != nil { return err } if response.Trace != nil { fmt.Println(string(*response.Trace)) } // Each node's input, output, and performance timing fmt.Println(response.Performance) // Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```go theme={null} import zen "github.com/gorules/zen-go/v2" // Standard expressions (with generics) result, _ := zen.EvaluateExpression[int]("a + b", map[string]any{"a": 5, "b": 3}) // => 8 total, _ := zen.EvaluateExpression[int]("sum(items)", map[string]any{"items": []int{1, 2, 3, 4}}) // => 10 // Unary expressions (comparison against $) isValid, _ := zen.EvaluateUnaryExpression(">= 5", map[string]any{"$": 10}) // => true inList, _ := zen.EvaluateUnaryExpression("'US', 'CA', 'MX'", map[string]any{"$": "US"}) // => true ``` ## Best practices **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Prefer built-in loaders.** `StaticLoader`, `FilesystemLoader`, and `ZipLoader` cover most setups without custom code; reserve callback loaders for backends the built-ins can't reach. **Call `Dispose()` on cleanup.** Release engine and decision resources when your application terminates. ```go theme={null} engine := zen.NewEngine(zen.EngineConfig{Loader: zen.Loader(loader)}) defer engine.Dispose() ``` **Use goroutines for parallel evaluation.** Decision evaluation is thread-safe and works well with concurrent workloads. ```go theme={null} var wg sync.WaitGroup for _, input := range inputs { wg.Add(1) go func(in map[string]any) { defer wg.Done() response, _ := engine.Evaluate("pricing.json", in) // process response }(input) } wg.Wait() ``` # iOS Rules Engine Source: https://docs.gorules.io/developers/sdks/ios Install the ZEN Engine Swift package and evaluate decisions in your iOS app. Install the ZEN Engine and evaluate your first decision in iOS. ## Installation ```swift theme={null} dependencies: [ .package(url: "https://github.com/gorules/zen-ios", from: "2.0.0") ] ``` Or in Xcode: File → Add Package Dependencies → Enter the repository URL. ## Basic usage ```swift theme={null} import UIKit import ZenUniffi class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Task { guard let ruleData = Bundle.main.url(forResource: "pricing", withExtension: "json") .flatMap({ try? Data(contentsOf: $0) }) else { return } let engine = try ZenEngine(loader: nil, customNode: nil) let decision = try engine.createDecision(content: ruleData) let input = """ { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } """.data(using: .utf8)! let response = try await decision.evaluate(context: input, options: nil) if let resultString = String(data: response.result, encoding: .utf8) { print(resultString) // => {"discount":0.15,"freeShipping":true} } } } } ``` ## Loader `ZenEngine` accepts an optional `ZenLoader` that serves decisions by key. Use the `static`, `filesystem`, or `zip` variants for common backends, or `callback` for custom loading logic. ### Static Register decisions in memory. Use this for rules shipped in your app bundle: ```swift theme={null} import ZenUniffi import Foundation func createEngine() throws -> ZenEngine { let url = Bundle.main.url(forResource: "pricing", withExtension: "json")! let pricing = try Data(contentsOf: url) let loader = ZenLoader.static(content: ["pricing.json": pricing]) return try ZenEngine(loader: loader, customNode: nil) } ``` ### File system Load decisions from files under a root directory, such as the Documents directory. Keys resolve to paths relative to the root: ```swift theme={null} import ZenUniffi import Foundation func createEngine() throws -> ZenEngine { let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let rulesURL = documentsURL.appendingPathComponent("rules") let loader = ZenLoader.filesystem(path: rulesURL.path) return try ZenEngine(loader: loader, customNode: nil) } ``` ### Zip archive Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release once and hand the bytes to the engine: ```swift theme={null} import ZenUniffi import Foundation func createEngine() async throws -> ZenEngine { let url = URL(string: "https://api.example.com/decisions.zip")! let (zipData, _) = try await URLSession.shared.data(from: url) let loader = ZenLoader.zip(bytes: zipData) return try ZenEngine(loader: loader, customNode: nil) } ``` ### Custom loader For any other backend, implement `ZenDecisionLoaderCallback` and wrap it in the `callback` variant: ```swift theme={null} import ZenUniffi import Foundation class BundleLoader: ZenDecisionLoaderCallback { func load(key: String) async throws -> Data? { guard let url = Bundle.main.url(forResource: key, withExtension: nil) else { return nil } return try Data(contentsOf: url) } } func createEngine() throws -> ZenEngine { let loader = ZenLoader.callback(callback: BundleLoader()) return try ZenEngine(loader: loader, customNode: nil) } ``` Returning `nil` from the callback reports the key as not found. ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```swift theme={null} let results = await engine.evaluateBatch( requests: [ ZenBatchRequest(key: "pricing.json", context: #"{ "amount": 100 }"#.data(using: .utf8)!), ZenBatchRequest(key: "pricing.json", context: #"{ "amount": 250 }"#.data(using: .utf8)!) ], options: nil ) for result in results { if result.success, let data = result.data, let resultString = String(data: data.result, encoding: .utf8) { print(resultString) } else { print("Evaluation failed: \(result.error ?? "unknown")") } } ``` ## Async/Await Evaluation functions are async, integrating natively with Swift concurrency: ```swift theme={null} import ZenUniffi func evaluateMultiple(decision: ZenDecision, inputs: [Data]) async throws -> [ZenEngineResponse] { try await withThrowingTaskGroup(of: ZenEngineResponse.self) { group in for input in inputs { group.addTask { try await decision.evaluate(context: input, options: nil) } } var results: [ZenEngineResponse] = [] for try await result in group { results.append(result) } return results } } ``` ## Error handling ```swift theme={null} import ZenUniffi do { let response = try await decision.evaluate(context: input, options: nil) if let resultString = String(data: response.result, encoding: .utf8) { print(resultString) } } catch let error as ZenError { print("Evaluation failed: \(error.localizedDescription)") } catch { print("Unexpected error: \(error)") } ``` ## Tracing Enable tracing to inspect decision execution: ```swift theme={null} import ZenUniffi let options = ZenEvaluateOptions(maxDepth: nil, trace: true) let response = try await decision.evaluate(context: input, options: options) if let trace = response.trace { for (nodeId, nodeTrace) in trace { print("[\(nodeTrace.order)] \(nodeTrace.name): \(String(data: nodeTrace.output, encoding: .utf8) ?? "")") } } print("Performance: \(response.performance)") ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```swift theme={null} import ZenUniffi // Standard expressions let result = try evaluateExpression( expression: "a + b", context: #"{ "a": 5, "b": 3 }"#.data(using: .utf8)! ) // => 8 let total = try evaluateExpression( expression: "sum(items)", context: #"{ "items": [1, 2, 3, 4] }"#.data(using: .utf8)! ) // => 10 // Unary expressions (comparison against $) let isValid = try evaluateUnaryExpression( expression: ">= 5", context: #"{ "$": 10 }"#.data(using: .utf8)! ) // => true let inList = try evaluateUnaryExpression( expression: "'US', 'CA', 'MX'", context: #"{ "$": "US" }"#.data(using: .utf8)! ) // => true ``` ## Best practices **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. ```swift theme={null} class RulesEngine { static let shared = RulesEngine() private let engine: ZenEngine private init() { engine = try! ZenEngine(loader: nil, customNode: nil) } func evaluate(decision: Data, context: Data) async throws -> Data { let dec = try engine.createDecision(content: decision) let response = try await dec.evaluate(context: context, options: nil) return response.result } } ``` **Cache decisions persistently.** Use the Caches or Documents directory to cache downloaded decisions for offline use. **Evaluate off the main thread.** Use `Task` or `Task.detached` to avoid blocking the main thread during evaluation. **Bundle fallback decisions.** Include decisions in your app bundle as fallback for first launch or network failures. # Java Rules Engine Source: https://docs.gorules.io/developers/sdks/java Integrate GoRules into your Java application. Install the ZEN Engine and evaluate your first decision in Java. ## Installation ```xml Maven theme={null} io.gorules zen-engine 2.0.0 ``` ```groovy Gradle theme={null} implementation("io.gorules:zen-engine:2.0.0") ``` The Java bindings require JDK 22 or newer. They use the FFM (Foreign Function & Memory) API to call the native Rust engine. On JDK 24+, add `--enable-native-access=ALL-UNNAMED` to your JVM options to silence native-access warnings. On versions 2.0.1 and earlier, the bundled native library must be provided manually: extract it from the jar for your platform and pass `-Duniffi.component.zen_uniffi.libraryOverride=/absolute/path/to/library`; newer versions extract it automatically. ## Basic usage ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenDecision; import io.gorules.zen_engine.JsonBuffer; public class Main { public static void main(String[] args) throws Exception { var ruleJson = Main.class.getResourceAsStream("/rules/pricing.json").readAllBytes(); try (var engine = new ZenEngine(null, null)) { var decision = engine.createDecision(new JsonBuffer(ruleJson)); var input = new JsonBuffer(""" { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } """); var response = decision.evaluate(input, null).join(); System.out.println(response.result()); // => {"discount":0.15,"freeShipping":true} } } } ``` ## Loader The `ZenEngine` constructor takes a `ZenLoader` that resolves decisions by key. `ZenLoader` is a sealed interface with four variants: `Callback` for loading from any storage backend, and the configurations `Static`, `Filesystem`, and `Zip`, which pre-load and pre-compile all decisions when you create the engine. ### Loader configurations Prefer a configuration when your decisions are known up front. The engine compiles them once, so evaluations skip loading and parsing entirely. ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenLoader; import io.gorules.zen_engine.JsonBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; // Static: decisions provided as an in-memory map var staticEngine = new ZenEngine(new ZenLoader.Static(Map.of( "pricing.json", new JsonBuffer(Files.readAllBytes(Path.of("./rules/pricing.json"))) )), null); // Filesystem: decisions resolved relative to a directory var fsEngine = new ZenEngine(new ZenLoader.Filesystem("./rules"), null); // Zip: decisions extracted from a zip archive var zipEngine = new ZenEngine(new ZenLoader.Zip(Files.readAllBytes(Path.of("./decisions.zip"))), null); var response = fsEngine.evaluate("pricing.json", new JsonBuffer("{}"), null).join(); System.out.println(response.result()); ``` ### Callback loader Use `ZenLoader.Callback` to load decisions from any storage backend. The callback returns `CompletableFuture`; complete it with `null` when the decision does not exist. Use `ConcurrentHashMap` to cache decisions for optimal performance. ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenLoader; import io.gorules.zen_engine.JsonBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; var cache = new ConcurrentHashMap(); var engine = new ZenEngine(new ZenLoader.Callback(key -> { var bytes = cache.computeIfAbsent(key, k -> { try { return Files.readAllBytes(Path.of("./rules", k)); } catch (Exception e) { return null; } }); // Completing with null maps to a "decision not found" error return CompletableFuture.completedFuture(bytes == null ? null : new JsonBuffer(bytes)); }), null); var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join(); System.out.println(response.result()); ``` ### AWS S3 ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenLoader; import io.gorules.zen_engine.JsonBuffer; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.regions.Region; var s3 = S3Client.builder().region(Region.US_EAST_1).build(); // Download all decisions as a zip archive at startup var request = GetObjectRequest.builder() .bucket("my-rules-bucket") .key("decisions.zip") .build(); var zipBytes = s3.getObjectAsBytes(request).asByteArray(); // ZenLoader.Zip extracts and pre-compiles every decision in the archive var engine = new ZenEngine(new ZenLoader.Zip(zipBytes), null); var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join(); System.out.println(response.result()); ``` ### Azure Blob Storage ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenLoader; import io.gorules.zen_engine.JsonBuffer; import com.azure.storage.blob.BlobServiceClientBuilder; var blobService = new BlobServiceClientBuilder() .connectionString(System.getenv("AZURE_STORAGE_CONNECTION")) .buildClient(); var container = blobService.getBlobContainerClient("rules"); // Download all decisions as a zip archive at startup var blob = container.getBlobClient("decisions.zip"); var zipBytes = blob.downloadContent().toBytes(); // ZenLoader.Zip extracts and pre-compiles every decision in the archive var engine = new ZenEngine(new ZenLoader.Zip(zipBytes), null); var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join(); System.out.println(response.result()); ``` ### Google Cloud Storage ```java theme={null} import io.gorules.zen_engine.ZenEngine; import io.gorules.zen_engine.ZenLoader; import io.gorules.zen_engine.JsonBuffer; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; var storage = StorageOptions.getDefaultInstance().getService(); // Download all decisions as a zip archive at startup var blob = storage.get("my-rules-bucket", "decisions.zip"); var zipBytes = blob.getContent(); // ZenLoader.Zip extracts and pre-compiles every decision in the archive var engine = new ZenEngine(new ZenLoader.Zip(zipBytes), null); var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join(); System.out.println(response.result()); ``` ## Async evaluation Evaluation returns `CompletableFuture` for non-blocking execution: ```java theme={null} import io.gorules.zen_engine.ZenEngineResponse; import java.util.concurrent.CompletableFuture; import java.util.List; import java.util.ArrayList; var futures = new ArrayList>(); for (var input : inputs) { futures.add(decision.evaluate(new JsonBuffer(input), null)); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); for (var future : futures) { System.out.println(future.get().result()); } ``` ## Batch evaluation Use `evaluateBatch` to evaluate many contexts in a single call. Each request pairs a decision key with a context, and each result reports its own success or error: ```java theme={null} import io.gorules.zen_engine.ZenBatchRequest; import io.gorules.zen_engine.JsonBuffer; import java.util.List; var requests = List.of( new ZenBatchRequest("pricing.json", new JsonBuffer("{\"order\": {\"subtotal\": 150}}")), new ZenBatchRequest("pricing.json", new JsonBuffer("{\"order\": {\"subtotal\": 40}}")) ); var results = engine.evaluateBatch(requests, null).join(); for (var result : results) { if (result.success()) { System.out.println(result.data().result()); } else { System.err.println("Evaluation failed: " + result.error()); } } ``` ## Error handling ```java theme={null} import io.gorules.zen_engine.ZenException; try { var response = decision.evaluate(input, null).join(); System.out.println(response.result()); } catch (Exception e) { if (e.getCause() instanceof ZenException zenEx) { System.err.println("Evaluation failed: " + zenEx.getMessage()); } else { throw e; } } ``` ## Tracing Enable tracing to inspect decision execution: ```java theme={null} import io.gorules.zen_engine.ZenEvaluateOptions; var options = new ZenEvaluateOptions(null, true); // (maxDepth, trace) - pass true to enable tracing var response = decision.evaluate(input, options).join(); System.out.println(response.trace()); // Each node's input, output, and performance timing System.out.println(response.performance()); // Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```java theme={null} import io.gorules.zen_engine.ZenUniffi; import io.gorules.zen_engine.JsonBuffer; // Standard expressions var context = new JsonBuffer(""" { "a": 5, "b": 3 } """); var result = ZenUniffi.evaluateExpression("a + b", context); // => 8 var itemsContext = new JsonBuffer(""" { "items": [1, 2, 3, 4] } """); var total = ZenUniffi.evaluateExpression("sum(items)", itemsContext); // => 10 // Unary expressions (comparison against $) var unaryContext = new JsonBuffer(""" { "$": 10 } """); var isValid = ZenUniffi.evaluateUnaryExpression(">= 5", unaryContext); // => true var listContext = new JsonBuffer(""" { "$": "US" } """); var inList = ZenUniffi.evaluateUnaryExpression("'US', 'CA', 'MX'", listContext); // => true ``` ## Runtime requirements The Java bindings use the FFM (Foreign Function & Memory) API from `java.lang.foreign` for interoperability with the native Rust engine. FFM requires JDK 22 or newer. ## Best practices **Use try-with-resources.** `ZenEngine` implements `AutoCloseable` to release native resources. ```java theme={null} try (var engine = new ZenEngine(null, null)) { // use engine } ``` **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching with `ConcurrentHashMap`. **Use `CompletableFuture` composition.** Chain async operations or use `allOf` for parallel evaluation of multiple decisions. # Kotlin Rules Engine Source: https://docs.gorules.io/developers/sdks/kotlin Integrate GoRules into your Kotlin application. Install the ZEN Engine and evaluate your first decision in Kotlin. ## Installation ```kotlin theme={null} dependencies { implementation("io.gorules:zen-engine-kotlin:2.0.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") } ``` ## Basic usage ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEngine import io.gorules.zen_engine.kotlin.JsonBuffer import kotlinx.coroutines.runBlocking fun main() = runBlocking { val ruleJson = object {}.javaClass.getResourceAsStream("/rules/pricing.json")!!.readBytes() ZenEngine(null, null).use { engine -> val decision = engine.createDecision(JsonBuffer(ruleJson)) val input = JsonBuffer(""" { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } """) val response = decision.evaluate(input, null) println(response.result) // => {"discount":0.15,"freeShipping":true} } } ``` ## Loader `ZenEngine` accepts an optional `ZenLoader` that serves decisions by key. Use `Static`, `Filesystem`, or `Zip` for common backends, or `Callback` for custom loading logic. ### Static Register decisions in memory. Use this when your rules ship with the application or are already loaded: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEngine import io.gorules.zen_engine.kotlin.ZenLoader import io.gorules.zen_engine.kotlin.JsonBuffer import java.nio.file.Files import java.nio.file.Path import kotlinx.coroutines.runBlocking fun main() = runBlocking { val pricing = Files.readString(Path.of("./rules/pricing.json")) val loader = ZenLoader.Static(mapOf("pricing.json" to JsonBuffer(pricing))) ZenEngine(loader, null).use { engine -> val response = engine.evaluate("pricing.json", JsonBuffer("""{ "amount": 100 }"""), null) println(response.result) } } ``` ### File system Load decisions from files under a root directory. Keys resolve to paths relative to the root: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEngine import io.gorules.zen_engine.kotlin.ZenLoader import io.gorules.zen_engine.kotlin.JsonBuffer import kotlinx.coroutines.runBlocking fun main() = runBlocking { val loader = ZenLoader.Filesystem("./rules") ZenEngine(loader, null).use { engine -> val response = engine.evaluate("pricing.json", JsonBuffer("""{ "amount": 100 }"""), null) println(response.result) } } ``` ### Zip archive Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release from object storage and hand the bytes to the engine: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEngine import io.gorules.zen_engine.kotlin.ZenLoader import io.gorules.zen_engine.kotlin.JsonBuffer import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.GetObjectRequest import kotlinx.coroutines.runBlocking fun main() = runBlocking { val s3 = S3Client.builder().region(Region.US_EAST_1).build() val request = GetObjectRequest.builder() .bucket("my-rules-bucket") .key("decisions.zip") .build() val zipBytes = s3.getObjectAsBytes(request).asByteArray() ZenEngine(ZenLoader.Zip(zipBytes), null).use { engine -> val response = engine.evaluate("pricing.json", JsonBuffer("""{ "amount": 100 }"""), null) println(response.result) } } ``` ### Custom loader For any other backend, implement `ZenDecisionLoaderCallback` and wrap it in `ZenLoader.Callback`: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEngine import io.gorules.zen_engine.kotlin.ZenLoader import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback import io.gorules.zen_engine.kotlin.JsonBuffer import java.nio.file.Files import java.nio.file.Path import kotlinx.coroutines.runBlocking val callback = object : ZenDecisionLoaderCallback { override suspend fun load(key: String): JsonBuffer? { val path = Path.of("./rules", key) if (!Files.exists(path)) return null return JsonBuffer(Files.readAllBytes(path)) } } fun main() = runBlocking { ZenEngine(ZenLoader.Callback(callback), null).use { engine -> val response = engine.evaluate("pricing.json", JsonBuffer("""{ "amount": 100 }"""), null) println(response.result) } } ``` Returning `null` from the callback reports the key as not found. ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```kotlin theme={null} val results = engine.evaluateBatch( listOf( ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 100 }""")), ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 250 }""")), ZenBatchRequest("eligibility.json", JsonBuffer("""{ "age": 30 }""")) ), null ) for (result in results) { if (result.success) { println(result.data?.result) } else { println("Evaluation failed: ${result.error}") } } ``` ## Coroutines Evaluation functions are `suspend` functions, integrating natively with Kotlin coroutines: ```kotlin theme={null} import kotlinx.coroutines.* coroutineScope { val results = inputs.map { input -> async { decision.evaluate(JsonBuffer(input), null) } }.awaitAll() results.forEach { println(it.result) } } ``` ## Error handling ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenException try { val response = decision.evaluate(input, null) println(response.result) } catch (e: ZenException) { println("Evaluation failed: ${e.message}") } ``` ## Tracing Enable tracing to inspect decision execution: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.ZenEvaluateOptions val options = ZenEvaluateOptions(trace = true, maxDepth = null) val response = decision.evaluate(input, options) println(response.trace) // Each node's input, output, and performance timing println(response.performance) // Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```kotlin theme={null} import io.gorules.zen_engine.kotlin.evaluateExpression import io.gorules.zen_engine.kotlin.evaluateUnaryExpression import io.gorules.zen_engine.kotlin.JsonBuffer // Standard expressions val result = evaluateExpression("a + b", JsonBuffer("""{ "a": 5, "b": 3 }""")) // => 8 val total = evaluateExpression("sum(items)", JsonBuffer("""{ "items": [1, 2, 3, 4] }""")) // => 10 // Unary expressions (comparison against $) val isValid = evaluateUnaryExpression(">= 5", JsonBuffer("""{ "$": 10 }""")) // => true val inList = evaluateUnaryExpression("'US', 'CA', 'MX'", JsonBuffer("""{ "$": "US" }""")) // => true ``` ## Performance note The Kotlin bindings use JNA (Java Native Access) for interoperability with the native Rust engine. This introduces some overhead compared to native Rust or direct bindings. We plan to revisit this when the FFM (Foreign Function & Memory) API becomes more widely adopted. ## Best practices **Use `.use {}` for resource management.** `ZenEngine` implements `AutoCloseable` to release native resources. ```kotlin theme={null} ZenEngine(null, null).use { engine -> // use engine } ``` **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Prefer declarative loaders.** `ZenLoader.Static`, `ZenLoader.Filesystem`, and `ZenLoader.Zip` serve decisions from native code without callback overhead. Reserve `ZenLoader.Callback` for backends the built-in variants don't cover. **Leverage coroutines for parallel evaluation.** Use `async`/`awaitAll` to evaluate multiple decisions concurrently. # Mobile Rules Engine Source: https://docs.gorules.io/developers/sdks/mobile Deploy business rules to mobile applications for offline-capable evaluation. The ZEN Engine runs natively on mobile devices, enabling offline rule evaluation without network round-trips. ## Why mobile rules? **Offline evaluation.** Rules execute locally on the device, working without network connectivity. **Low latency.** No network round-trips means instant evaluation, critical for responsive UX. **Reduced server load.** Offload rule evaluation to client devices, reducing backend infrastructure costs. **Privacy.** Sensitive data never leaves the device when rules are evaluated locally. ## Architecture patterns ### Bundled decisions Ship decision files with your app bundle. Best for rules that change infrequently. **Pros:** Always available, no network dependency, fastest startup. **Cons:** Requires app update to change rules. ### Remote decisions with caching Fetch decisions from a remote source and cache locally. Best for rules that update periodically. ```mermaid theme={null} flowchart LR A[App Start] --> B[Cache
Local] B --> C[Remote Source] B --> D[Evaluate Locally] ``` **Pros:** Rules can be updated without app releases, offline fallback. **Cons:** More complex, requires cache invalidation strategy. ### Firebase Remote Config Use Firebase to distribute decision files across your mobile fleet. ```mermaid theme={null} flowchart LR A[BRMS Publish] --> B[Firebase
Remote Config] --> C[Mobile App] ``` **Setup:** 1. Export decision JSON from BRMS 2. Upload to Firebase Remote Config as a string parameter 3. Fetch and cache on app startup 4. Evaluate locally using the SDK **Benefits:** * Gradual rollouts and A/B testing * Instant updates without app store review * Built-in caching and offline support * Analytics integration ### Cloud Storage distribution Store decisions in cloud storage (S3, GCS, Azure Blob) and sync to devices. **Setup:** 1. Configure BRMS to publish to cloud storage 2. Mobile app checks for updates on startup or periodically 3. Download and cache new versions locally 4. Evaluate using cached decisions Release archives pair naturally with `ZenLoader.Zip`: download the zip once and pass its bytes to the engine, which extracts and pre-compiles every decision. ## Offline-first patterns ### Cache-first strategy Always evaluate using cached decisions, update cache in background. ```mermaid theme={null} flowchart LR subgraph Foreground A[App Starts] --> B[Load from Cache] --> C[Ready to Evaluate] end subgraph Background D[Check for Updates] --> E[Download if Newer] end ``` ### Version checking Include version metadata to minimize unnecessary downloads. ```json theme={null} { "version": "1.2.3", "updated": "2024-01-15T10:30:00Z", "decisions": ["pricing.json", "eligibility.json"] } ``` Check version before downloading full decision files. ### Fallback hierarchy ```mermaid theme={null} flowchart TD A[Try Remote Decision] -->|Fail| B[Use Cached Decision] B -->|Fail| C[Use Bundled Decision] A -->|Success| D[Evaluate] B -->|Success| D C --> D ``` ## Platform SDKs Native Android SDK with Kotlin coroutines support Native iOS SDK with Swift concurrency support ## Best practices **Bundle a fallback.** Always include a bundled decision as fallback for first launch or network failures. **Cache aggressively.** Decision files are typically small; cache them persistently rather than in memory only. **Version your decisions.** Include version metadata to enable efficient cache invalidation. **Handle errors gracefully.** Network failures are common on mobile; design for offline-first. **Test offline scenarios.** Simulate airplane mode and poor connectivity during development. # Node.js Rules Engine Source: https://docs.gorules.io/developers/sdks/nodejs Integrate GoRules into your Node.js application. Install the ZEN Engine and evaluate your first decision in Node.js. ## Installation ```bash npm theme={null} npm install @gorules/zen-engine ``` ```bash yarn theme={null} yarn add @gorules/zen-engine ``` ```bash pnpm theme={null} pnpm add @gorules/zen-engine ``` ## Basic usage ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; import fs from 'fs'; const content = fs.readFileSync('./pricing-rules.json'); const engine = new ZenEngine(); const decision = engine.createDecision(content); const response = await decision.evaluate({ customer: { tier: 'gold', yearsActive: 3 }, order: { subtotal: 150, items: 5 } }); console.log(response.result); // => { discount: 0.15, freeShipping: true } engine.dispose(); ``` ## Loaders Loaders let the engine resolve decisions by key - `engine.evaluate('pricing.json', ...)` - instead of you reading files by hand. Pass one to the `ZenEngine` constructor: either a built-in loader config (`static`, `fs`, or `zip`) or a custom async function. ### Static The static loader serves decisions from an in-memory map. Use it when your rules ship with the application or arrive as one payload: ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; import pricingRules from './pricing-rules.json' with { type: 'json' }; const engine = new ZenEngine({ loader: { type: 'static', content: { 'pricing.json': pricingRules } } }); const response = await engine.evaluate('pricing.json', { amount: 100 }); console.log(response.result); ``` ### File system The `fs` loader resolves keys against a root directory - `pricing.json` maps to `./rules/pricing.json`: ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; const engine = new ZenEngine({ loader: { type: 'fs', path: './rules' } }); const response = await engine.evaluate('pricing.json', { amount: 100 }); ``` ### Zip archive The `zip` loader unpacks an archive in memory; every `.json` entry becomes a decision keyed by its path in the archive. This pairs naturally with [release ZIPs](/brms/deploy/releases) downloaded from the BRMS or object storage: ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'us-east-1' }); const s3Response = await s3.send(new GetObjectCommand({ Bucket: 'my-rules-bucket', Key: 'release-1.0.0.zip' })); const engine = new ZenEngine({ loader: { type: 'zip', bytes: Buffer.from(await s3Response.Body.transformToByteArray()) } }); const response = await engine.evaluate('pricing.json', { amount: 100 }); ``` ### Custom loader For any other backend - a database, a remote API, per-tenant storage - pass an async function. Combine it with `ZenDecisionContent` to cache pre-compiled decisions: ```javascript theme={null} import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine'; import fs from 'fs/promises'; import path from 'path'; const cache = new Map(); const engine = new ZenEngine({ loader: async (key) => { if (cache.has(key)) { return cache.get(key); } const buffer = await fs.readFile(path.join('./rules', key)); const content = new ZenDecisionContent(buffer); cache.set(key, content); return content; } }); const response = await engine.evaluate('pricing.json', { amount: 100 }); ``` ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```javascript theme={null} const results = await engine.evaluateBatch([ { key: 'pricing.json', context: { amount: 100 } }, { key: 'pricing.json', context: { amount: 250 } }, { key: 'eligibility.json', context: { age: 30 } } ]); for (const result of results) { if (result.success) { console.log(result.data.result); } else { console.error('Evaluation failed:', result.error); } } ``` ## Error handling Using try-catch: ```javascript theme={null} try { const response = await decision.evaluate(input); console.log(response.result); } catch (error) { console.error('Evaluation failed:', error.message); } ``` Using `safeEvaluate`: ```javascript theme={null} const response = await decision.safeEvaluate(input); if (response.success) { console.log(response.data.result); } else { console.error('Evaluation failed:', response.error); } ``` ## Tracing Enable tracing to inspect decision execution: ```javascript theme={null} const response = await decision.evaluate(input, { trace: true }); console.log(response.trace); // Each node's input, output, and performance timing console.log(response.performance); // Total evaluation time ``` ## Engine configuration Override process-wide engine defaults with `overrideConfig`. Call it once at startup, before creating engines: ```javascript theme={null} import { overrideConfig } from '@gorules/zen-engine'; overrideConfig({ functionTimeoutMillis: 10000, nodesInContext: false, httpAuth: false, }); ``` * `functionTimeoutMillis` — wall-clock limit for function nodes. Defaults to `5000`. * `nodesInContext` — expose `$nodes` results to downstream nodes. Defaults to `true`; disable to reduce context size. * `httpAuth` — allow IAM-signed requests (AWS, Azure, GCP) from the function node `http` module. Defaults to `true`. ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```javascript theme={null} import { evaluateExpression, evaluateUnaryExpression } from '@gorules/zen-engine'; // Standard expressions const sum = await evaluateExpression('a + b', { a: 5, b: 3 }); // => 8 const total = await evaluateExpression('sum(items)', { items: [1, 2, 3, 4] }); // => 10 // Unary expressions (comparison against $) const isValid = await evaluateUnaryExpression('>= 5', { $: 10 }); // => true const inList = await evaluateUnaryExpression('"US", "CA", "MX"', { $: 'US' }); // => true ``` Synchronous versions are also available: ```javascript theme={null} import { evaluateExpressionSync, evaluateUnaryExpressionSync } from '@gorules/zen-engine'; const result = evaluateExpressionSync('a * 2', { a: 10 }); // => 20 ``` ## Best practices **Use `ZenDecisionContent` for caching.** Pre-compiling decisions avoids repeated parsing overhead. Cache compiled content in a `Map` keyed by decision name. **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Prefer built-in loaders.** The `static`, `fs`, and `zip` loaders cover most setups without custom code; reserve callback loaders for backends the built-ins can't reach. **Call `dispose()` on shutdown.** Release engine resources when your application terminates to prevent memory leaks. # Python Rules Engine Source: https://docs.gorules.io/developers/sdks/python Integrate GoRules into your Python application. Install the ZEN Engine and evaluate your first decision in Python. ## Installation ```bash pip theme={null} pip install zen-engine ``` ```bash poetry theme={null} poetry add zen-engine ``` ```bash uv theme={null} uv add zen-engine ``` ## Basic usage ```python theme={null} import zen with open('./pricing-rules.json') as f: content = f.read() engine = zen.ZenEngine() decision = engine.create_decision(content) response = decision.evaluate({ "customer": {"tier": "gold", "yearsActive": 3}, "order": {"subtotal": 150, "items": 5} }) print(response["result"]) # => {'discount': 0.15, 'freeShipping': True} ``` ## Loader Pass a `loader` option to `ZenEngine` to serve decisions by key. Use a declarative config for common backends, or a callback for custom ones. ### Static Register decisions in memory. Use this when your rules ship with the application or are already loaded: ```python theme={null} import json import zen with open("./rules/pricing.json") as f: pricing = json.load(f) engine = zen.ZenEngine({ "loader": {"type": "static", "content": {"pricing.json": pricing}} }) response = engine.evaluate("pricing.json", {"amount": 100}) print(response["result"]) ``` ### File system Load decisions from files under a root directory. Keys resolve to paths relative to `path`: ```python theme={null} import zen engine = zen.ZenEngine({"loader": {"type": "fs", "path": "./rules"}}) response = engine.evaluate("pricing.json", {"amount": 100}) print(response["result"]) ``` ### Zip archive Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release from object storage and hand the bytes to the engine: ```python theme={null} import boto3 import zen s3 = boto3.client("s3") obj = s3.get_object(Bucket="my-rules-bucket", Key="decisions.zip") engine = zen.ZenEngine({"loader": {"type": "zip", "bytes": obj["Body"].read()}}) response = engine.evaluate("pricing.json", {"amount": 100}) print(response["result"]) ``` ### Custom loader For any other backend, pass a callback that receives the decision key and returns its content: ```python theme={null} import zen def loader(key): with open(f"./rules/{key}") as f: return f.read() engine = zen.ZenEngine({"loader": loader}) response = engine.evaluate("pricing.json", {"amount": 100}) print(response["result"]) ``` The callback can also be an `async` function - see [Async support](#async-support). ## Batch evaluation Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch: ```python theme={null} results = engine.evaluate_batch([ {"key": "pricing.json", "context": {"amount": 100}}, {"key": "pricing.json", "context": {"amount": 250}}, {"key": "eligibility.json", "context": {"age": 30}}, ]) for result in results: if result["success"]: print(result["data"]["result"]) else: print("Evaluation failed:", result["error"]) ``` ## Async support Use `async_evaluate` for non-blocking evaluation: ```python theme={null} import asyncio import zen async def loader(key): with open(f"./rules/{key}") as f: return f.read() engine = zen.ZenEngine({"loader": loader}) async def main(): # Evaluate multiple decisions concurrently results = await asyncio.gather( engine.async_evaluate("pricing.json", {"amount": 100}), engine.async_evaluate("eligibility.json", {"score": 750}), engine.async_evaluate("shipping.json", {"weight": 5}) ) for response in results: print(response["result"]) asyncio.run(main()) ``` ## Error handling ```python theme={null} try: response = decision.evaluate(input_data) print(response["result"]) except Exception as e: print(f"Evaluation failed: {e}") ``` ## Tracing Enable tracing to inspect decision execution: ```python theme={null} response = decision.evaluate(input_data, {"trace": True}) print(response["trace"]) # Each node's input, output, and performance timing print(response["performance"]) # Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```python theme={null} import zen # Standard expressions result = zen.evaluate_expression("a + b", {"a": 5, "b": 3}) # => 8 total = zen.evaluate_expression("sum(items)", {"items": [1, 2, 3, 4]}) # => 10 # Unary expressions (comparison against $) is_valid = zen.evaluate_unary_expression(">= 5", {"$": 10}) # => True in_list = zen.evaluate_unary_expression("'US', 'CA', 'MX'", {"$": "US"}) # => True ``` Compile expressions for repeated evaluation: ```python theme={null} import zen expr = zen.compile_expression("a * b + c") result1 = expr.evaluate({"a": 2, "b": 3, "c": 4}) # => 10 result2 = expr.evaluate({"a": 5, "b": 6, "c": 7}) # => 37 ``` ## Spark integration For distributed processing at scale, see [PySpark](/developers/integrations/pyspark) and [AWS Glue](/developers/integrations/aws-glue). ## Best practices **Use `ZenDecisionContent` for caching.** Pre-compiling decisions avoids repeated parsing overhead. Cache compiled content in a dict keyed by decision name. **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching at the source. **Use async evaluation for concurrent workloads.** When evaluating multiple decisions, use `async_evaluate` with `asyncio.gather` for better throughput. # Rust Rules Engine Source: https://docs.gorules.io/developers/sdks/rust Integrate GoRules into your Rust application for maximum performance. The ZEN Engine is written in Rust, giving you direct access to the core engine with zero FFI overhead. ## Installation Add to your `Cargo.toml`: ```toml theme={null} [dependencies] zen-engine = "2" zen-expression = "2" serde_json = "1.0" tokio = { version = "1", features = ["full"] } ``` **Upgrading from 0.x?** `arbitrary_precision` is no longer a default feature. If you rely on arbitrary-precision number handling, enable it explicitly: `zen-engine = { version = "2", features = ["arbitrary_precision"] }`. Language bindings are unaffected. ## Basic usage ```rust theme={null} use zen_engine::DecisionEngine; use zen_engine::model::DecisionContent; use serde_json::json; #[tokio::main] async fn main() { let decision_content: DecisionContent = serde_json::from_str(include_str!("./pricing-rules.json")).unwrap(); let engine = DecisionEngine::default(); let decision = engine.create_decision(decision_content.into()).unwrap(); let response = decision.evaluate(json!({ "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } }).into()).await.unwrap(); println!("{}", response.result); // => {"discount":0.15,"freeShipping":true} } ``` ## Loader Attach a loader to serve decisions by key. Build one declaratively from `LoaderConfig`, or construct the loader structs in `zen_engine::loader` directly. ### Static Serve decisions from an in-memory map. `LoaderConfig::Static` builds a `MemoryLoader` under the hood: ```rust theme={null} use std::collections::HashMap; use zen_engine::DecisionEngine; use zen_engine::loader::LoaderConfig; use zen_engine::model::DecisionContent; use serde_json::json; #[tokio::main] async fn main() { let pricing: DecisionContent = serde_json::from_str(&std::fs::read_to_string("./rules/pricing.json").unwrap()).unwrap(); let mut content = HashMap::new(); content.insert("pricing.json".to_string(), pricing); let loader = LoaderConfig::Static { content }.into_loader().unwrap(); let engine = DecisionEngine::default().with_loader(loader); let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap(); println!("{}", response.result); } ``` To add or remove decisions at runtime, use `MemoryLoader` directly - it exposes `add`, `get`, and `remove`. ### File system Load decisions from files under a root directory. Keys resolve to paths relative to the root: ```rust theme={null} use zen_engine::DecisionEngine; use zen_engine::loader::LoaderConfig; use serde_json::json; #[tokio::main] async fn main() { let loader = LoaderConfig::Filesystem { path: "./rules".to_string() } .into_loader() .unwrap(); let engine = DecisionEngine::default().with_loader(loader); let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap(); println!("{}", response.result); } ``` `FilesystemLoader::new(FilesystemLoaderOptions { root })` is the equivalent direct construction. ### Zip archive Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release from object storage and hand the bytes to the engine: ```rust theme={null} use zen_engine::DecisionEngine; use zen_engine::loader::LoaderConfig; use serde_json::json; #[tokio::main] async fn main() { // Bytes of a release ZIP, e.g. downloaded from object storage let bytes = std::fs::read("./decisions.zip").unwrap(); let loader = LoaderConfig::Zip { bytes }.into_loader().unwrap(); let engine = DecisionEngine::default().with_loader(loader); let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap(); println!("{}", response.result); } ``` ### Custom loader Define custom loading logic with an async closure: ```rust theme={null} use zen_engine::DecisionEngine; use zen_engine::loader::LoaderError; use zen_engine::model::DecisionContent; use serde_json::json; use std::sync::Arc; #[tokio::main] async fn main() { let engine = DecisionEngine::default().with_closure_loader(|key| async move { // Load from any source: HTTP, S3, database, etc. let data = std::fs::read(format!("./rules/{key}")) .map_err(|_| LoaderError::NotFound(key.clone()))?; let content: DecisionContent = serde_json::from_slice(&data) .map_err(|source| LoaderError::Internal { key, source: source.into() })?; Ok(Arc::new(content)) }); let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap(); println!("{}", response.result); } ``` For full control, implement the `DecisionLoader` trait. Wrap any loader in `CachedLoader` to add an in-memory cache. ## Pre-compilation Pre-compile decisions for improved evaluation performance: ```rust theme={null} let mut decision = engine.create_decision(content.into()).unwrap(); decision.compile(); // Subsequent evaluations are faster let response = decision.evaluate(input.into()).await.unwrap(); ``` Compilation parses and optimizes the decision graph ahead of time, reducing overhead during evaluation. This is especially beneficial when the same decision is evaluated many times. ## Error handling ```rust theme={null} use zen_engine::EvaluationError; match decision.evaluate(input).await { Ok(response) => println!("{}", response.result), Err(e) => eprintln!("Evaluation failed: {:?}", e), } ``` ## Tracing Enable tracing to inspect decision execution: ```rust theme={null} use zen_engine::EvaluationOptions; let response = decision.evaluate_with_opts(input, EvaluationOptions { trace: true, ..Default::default() }).await.unwrap(); println!("{:?}", response.trace); // Each node's input, output, and performance timing println!("{}", response.performance); // Total evaluation time ``` ## Expression utilities The `zen-expression` crate provides expression evaluation outside of decisions: ```rust theme={null} use zen_expression::{evaluate_expression, evaluate_unary_expression}; use serde_json::json; // Standard expressions let result = evaluate_expression("a + b", json!({ "a": 5, "b": 3 }).into()).unwrap(); // => 8 let total = evaluate_expression("sum(items)", json!({ "items": [1, 2, 3, 4] }).into()).unwrap(); // => 10 // Unary expressions (comparison against $) let is_valid = evaluate_unary_expression(">= 5", json!({ "$": 10 }).into()).unwrap(); // => true let in_list = evaluate_unary_expression("'US', 'CA', 'MX'", json!({ "$": "US" }).into()).unwrap(); // => true ``` ### High performance with Isolate For repeated evaluations, use `Isolate` to reuse allocated memory: ```rust theme={null} use zen_expression::Isolate; use serde_json::json; let context = json!({ "tax": { "percentage": 10 } }); let mut isolate = Isolate::with_environment(context.into()); // Reuses memory across evaluations for amount in [50, 100, 150, 200] { let tax = isolate.run_standard(&format!("{} * tax.percentage / 100", amount)).unwrap(); println!("Tax on {}: {}", amount, tax); } ``` ## Design notes ### Single-threaded expression engine The expression engine is single-threaded by design for maximum performance. This avoids synchronization overhead and enables optimizations like memory reuse in `Isolate`. ### Thread-pinned futures Although `evaluate` is async, the returned `Future` is `!Send` - it must complete on the same thread where it was started. This is intentional: sending data across threads would be costly in this scenario, and pinning enables significant performance gains. However, this can be awkward with async runtimes that expect `Send` futures. For multi-threaded workloads, use `LocalPoolHandle` from `tokio-util` to spawn pinned tasks: ```rust theme={null} use std::future::Future; use std::sync::OnceLock; use std::thread::available_parallelism; use tokio::task::JoinHandle; use tokio_util::task::LocalPoolHandle; fn parallelism() -> usize { available_parallelism().map(Into::into).unwrap_or(1) } fn worker_pool() -> LocalPoolHandle { static LOCAL_POOL: OnceLock = OnceLock::new(); LOCAL_POOL .get_or_init(|| LocalPoolHandle::new(parallelism())) .clone() } fn spawn_pinned(create_task: F) -> JoinHandle where F: FnOnce() -> Fut + Send + 'static, Fut: Future + 'static, Fut::Output: Send + 'static, { worker_pool().spawn_pinned(create_task) } ``` Usage: ```rust theme={null} let result = spawn_pinned(|| async { let engine = DecisionEngine::default(); let decision = engine.create_decision(content.into()).unwrap(); decision.evaluate(input.into()).await }).await.unwrap(); ``` ## Best practices **Prefer the static or zip loader in production.** Both are backed by `MemoryLoader`, so decisions are parsed once and served from memory. Wrap other loaders in `CachedLoader` to get the same effect. **Initialize the engine once.** Create a single `DecisionEngine` instance at application startup and reuse it for all evaluations. **Use `Isolate` for repeated expression evaluation.** It reuses allocated memory, drastically improving throughput. # Swift Rules Engine Source: https://docs.gorules.io/developers/sdks/swift Execute business rules in Swift with the open-source ZEN Engine SDK. The Swift SDK allows you to execute business rules directly within your iOS application. macOS is not currently supported. See the [iOS SDK documentation](/developers/sdks/ios) to get started. # WASM Rules Engine Source: https://docs.gorules.io/developers/sdks/wasm Run the GoRules engine entirely in the browser using WebAssembly. The ZEN Engine compiles to WebAssembly, allowing you to evaluate decisions entirely in the browser with no backend required. ## Installation ```bash theme={null} npm install @gorules/zen-engine --cpu=wasm32 ``` For yarn v4, configure `supportedArchitectures` in `.yarnrc.yml`: ```yaml .yarnrc.yml theme={null} supportedArchitectures: cpu: - current - wasm32 ``` Then install: ```bash theme={null} yarn add @gorules/zen-engine ``` For yarn v1, use `--ignore-engines` as there is no other effective way to install wasm32 packages: ```bash theme={null} yarn add @gorules/zen-engine --ignore-engines ``` Configure `supportedArchitectures` in `pnpm-workspace.yaml`: ```yaml pnpm-workspace.yaml theme={null} supportedArchitectures: cpu: - current - wasm32 ``` Then install: ```bash theme={null} pnpm add @gorules/zen-engine ``` ## Server requirements WASM requires `SharedArrayBuffer` support, which needs these HTTP headers: ``` Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin ``` Example Vite configuration: ```javascript vite.config.ts theme={null} import { defineConfig } from 'vite' export default defineConfig({ plugins: [ { name: 'configure-response-headers', enforce: 'pre', configureServer: (server) => { server.middlewares.use((_req, res, next) => { res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') res.setHeader('Cross-Origin-Opener-Policy', 'same-origin') next() }) }, }, ], }) ``` ## Basic usage ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; const res = await fetch('/rules/pricing.json'); const content = await res.json(); const engine = new ZenEngine(); const decision = engine.createDecision(content); const response = await decision.evaluate({ customer: { tier: 'gold', yearsActive: 3 }, order: { subtotal: 150, items: 5 } }); console.log(response.result); // => { discount: 0.15, freeShipping: true } engine.dispose(); ``` ## Loader The loader pattern enables dynamic decision loading from remote sources. Combined with `ZenDecisionContent` for pre-compilation, this provides optimal performance for multi-decision applications. ```javascript theme={null} import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine'; const cache = new Map(); const engine = new ZenEngine({ loader: async (key) => { if (cache.has(key)) { return cache.get(key); } const response = await fetch(`/rules/${key}`); const buffer = await response.arrayBuffer(); const content = new ZenDecisionContent(new Uint8Array(buffer)); cache.set(key, content); return content; } }); const response = await engine.evaluate('pricing.json', { amount: 100 }); console.log(response.result); ``` ### Loader configurations Instead of a callback, `loader` also accepts a configuration object. The engine pre-loads and pre-compiles every decision at construction, so evaluations skip loading and parsing entirely: ```javascript theme={null} import { ZenEngine } from '@gorules/zen-engine'; // Static: decisions provided up front const res = await fetch('/rules/pricing.json'); const staticEngine = new ZenEngine({ loader: { type: 'static', content: { 'pricing.json': await res.json() } } }); // Zip: decisions extracted from a zip archive, such as a BRMS release const zipRes = await fetch('/rules/decisions.zip'); const zipEngine = new ZenEngine({ loader: { type: 'zip', bytes: new Uint8Array(await zipRes.arrayBuffer()) } }); const response = await zipEngine.evaluate('pricing.json', { amount: 100 }); console.log(response.result); ``` ## Batch evaluation Use `evaluateBatch` to evaluate many contexts in a single call. Each result reports its own success or error: ```javascript theme={null} const results = await engine.evaluateBatch([ { key: 'pricing.json', context: { amount: 100 } }, { key: 'pricing.json', context: { amount: 250 } } ]); for (const result of results) { if (result.success) { console.log(result.data.result); } else { console.error('Evaluation failed:', result.error); } } ``` ## HTTP handler When decisions make HTTP requests to external APIs, use `httpHandler` to proxy requests through your backend. This is necessary when the frontend cannot directly access services behind a firewall or private network: ```javascript theme={null} const engine = new ZenEngine({ httpHandler: async (request) => { const response = await fetch('/api/proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); const data = await response.json(); return { status: response.status, headers: Object.fromEntries(response.headers.entries()), data }; } }); ``` Your backend proxy can forward requests to internal services, add authentication headers, or handle IAM credentials. ## Error handling Using try-catch: ```javascript theme={null} try { const response = await decision.evaluate(input); console.log(response.result); } catch (error) { console.error('Evaluation failed:', error.message); } ``` Using `safeEvaluate`: ```javascript theme={null} const response = await decision.safeEvaluate(input); if (response.success) { console.log(response.data.result); } else { console.error('Evaluation failed:', response.error); } ``` ## Tracing Enable tracing to inspect decision execution: ```javascript theme={null} const response = await decision.evaluate(input, { trace: true }); console.log(response.trace); // Each node's input, output, and performance timing console.log(response.performance); // Total evaluation time ``` ## Expression utilities Evaluate ZEN expressions outside of a decision context: ```javascript theme={null} import { evaluateExpression, evaluateUnaryExpression } from '@gorules/zen-engine'; // Standard expressions const sum = await evaluateExpression('a + b', { a: 5, b: 3 }); // => 8 const total = await evaluateExpression('sum(items)', { items: [1, 2, 3, 4] }); // => 10 // Unary expressions (comparison against $) const isValid = await evaluateUnaryExpression('>= 5', { $: 10 }); // => true const inList = await evaluateUnaryExpression('"US", "CA", "MX"', { $: 'US' }); // => true ``` Synchronous versions are also available: ```javascript theme={null} import { evaluateExpressionSync, evaluateUnaryExpressionSync } from '@gorules/zen-engine'; const result = evaluateExpressionSync('a * 2', { a: 10 }); // => 20 ``` ## Best practices **Use `ZenDecisionContent` for caching.** Pre-compiling decisions avoids repeated parsing overhead. Cache compiled content in a `Map` keyed by decision name. **Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations. **Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching at the source. **Call `dispose()` on cleanup.** Release engine resources when the application terminates to prevent memory leaks. # Overview Source: https://docs.gorules.io/index Define rules visually or programmatically, test instantly, and deploy without rewriting software. ## Explore the documentation Understand GoRules fundamentals, create your first rule, and explore tutorials for common use cases like pricing, eligibility, and risk scoring. Integrate the ZEN Engine into your applications using SDKs for Node.js, Python, Go, Rust, and more. Deploy as embedded, agent, or self-hosted. Manage the Business Rules Management System - set up organizations, create projects, manage environments, and collaborate with your team. *** ## Choose your path Create, test, and manage business rules using a visual interface - no coding required. **Start here:** * [What is GoRules?](/learn/getting-started/what-is-gorules) - Core concepts * [Create your first rule](/learn/getting-started/first-rule) - Quickstart * [Try the playground](/learn/getting-started/playground) - Interactive demo Integrate the rules engine into your application using SDKs or REST API. **Start here:** * [Architecture overview](/developers/overview/architecture) - How it works * [Choose an SDK](/developers/sdks/nodejs) - Node.js, Python, Go, Rust, Java * [Deployment options](/developers/deployment/embedded) - Embedded, Agent, BRMS *** ## What's in each section ### Learn Master the fundamentals and build real-world rules. | Section | Description | | --------------------------------------------------------- | ----------------------------------------------------------------------- | | [Getting Started](/learn/getting-started/what-is-gorules) | Core concepts, first rule, and interactive playground | | [Authoring](/learn/authoring/decision-tables) | Decision tables, graphs, expressions, function nodes, testing, patterns | | [Tutorials](/learn/tutorials/dynamic-pricing) | Step-by-step guides for pricing, eligibility, risk scoring, commissions | | [ZEN Language](/learn/zen-language/syntax) | Expression syntax, operators, built-in functions, date handling | | [JDM Format](/learn/jdm-format/standard) | JSON Decision Model specification and node types | ### Developers Integrate, deploy, and scale GoRules in your infrastructure. | Section | Description | | ------------------------------------------------------------- | ----------------------------------------------------------- | | [Overview](/developers/overview/architecture) | Architecture, disaster recovery, BRE vs BRMS comparison | | [SDKs](/developers/sdks/nodejs) | Node.js, Python, Go, Rust, Java, Kotlin, Swift integrations | | [Deployment](/developers/deployment/embedded) | Embedded engine, standalone Agent, or self-hosted BRMS | | [Platform Guides](/developers/platform-guides/docker-compose) | Docker Compose, Kubernetes, AWS ECS, Azure Container Apps | | [Tools](/developers/jdm/jdm-editor) | JDM Editor component, standalone editor | ### BRMS User Guide Administer and collaborate on your Business Rules Management System. | Section | Description | | ----------------------------------------------------- | ----------------------------------------------------------------- | | [Setup](/brms/setup/projects) | Organization setup, projects, users, and groups | | [Build](/brms/build/workspace) | Workspace, graphs, policies, tests, and GoRules AI | | [Quality](/brms/quality/quality-control) | Test runs, coverage, and static analysis | | [Review & Ship](/brms/review/review) | Pre-flight checks, branches, requests, releases, and environments | | [Administration](/brms/administration/members-groups) | User management and audit logs | | [Deployments](/brms/setup/deployments) | Connect and configure runtime deployments | *** ## Get help Open source ZEN Engine repository - issues, discussions, and contributions. Submit tickets via Jira Service Desk. Available for Business and Enterprise plans. Enterprise customers have access to a dedicated support channel via Slack or Microsoft Teams. # Understanding the decision graph Source: https://docs.gorules.io/learn/authoring/decision-graphs Learn how to use the visual canvas to build decision logic by connecting nodes. The decision graph is GoRules' visual canvas for modeling business logic. You build decisions by placing nodes on the canvas and connecting them to define how data flows through your rules. ## The canvas When you create a new decision, you start with a blank canvas. Build your decision by adding nodes and connecting them. Data flows left to right through your graph: * **Input node** - Where data enters your decision (required) * **Processing nodes** - Decision tables, expressions, functions, switches * **Output node** - Optional; without one, results come from all endpoint nodes ## Adding nodes Drag nodes from the palette onto the canvas: 1. Drag a node from node palette 2. Drop it on the canvas 3. Connect it to other nodes by dragging from output ports to input ports ## Node types | Node | Purpose | Use when | | ------------------ | ----------------------------------- | ---------------------------------------------------- | | **Decision Table** | Spreadsheet-style conditional logic | You have multiple rules with conditions and outcomes | | **Expression** | Transform and calculate data | You need to compute values or reshape data | | **Function** | Custom JavaScript logic | You need complex calculations or external calls | | **Switch** | Route data to different paths | Different inputs need different processing | ### Switch node Use switch nodes to control the flow of your decision graph. Each branch has a condition - data flows down the first branch that matches. **First hit (default)** - Executes only the first matching branch: **Collect** - Executes all matching branches and combines results: ## Connecting nodes Click and drag from a node's output port (right side) to another node's input port (left side). The connection shows data flow direction. Nodes can have multiple incoming connections - data from all sources merges before processing. Nodes can also have multiple outgoing connections - the same output goes to all connected nodes. ### How data merges When multiple nodes connect to a single node, their outputs are merged into one object. Later connections overwrite earlier ones if they have the same field names. ``` [Discounts] outputs: { discount: 0.15, reason: "loyalty" } [Shipping] outputs: { shipping: 9.99, method: "standard" } ↓ both connect to ↓ [Calculate Total] receives: { discount: 0.15, reason: "loyalty", shipping: 9.99, method: "standard" } ``` If you need to avoid conflicts, use unique field names or `outputPath` to namespace each node's output (see [Patterns](/learn/authoring/patterns#organizing-output-with-outputpath)). ## Data flow When you evaluate a decision: 1. Input data enters through the Input node 2. Each connected node processes the data in sequence 3. Results pass through connections to downstream nodes 4. Results return from all endpoint nodes (or the Output node if you have one) The highlighted **Calculate Total** node receives data from both the original input and the discount table's output. If a node has multiple inputs, data from all sources is merged. ## Organizing your graph For complex decisions: * **Arrange left to right** - Keep the flow direction consistent * **Group related logic** - Place similar operations near each other * **Use meaningful names** - Click on node's name to rename it ## Keyboard shortcuts | Shortcut | Action | | -------------- | -------------------- | | `Delete` | Remove selected node | | `Cmd/Ctrl + C` | Copy selected | | `Cmd/Ctrl + V` | Paste | # Building decision tables Source: https://docs.gorules.io/learn/authoring/decision-tables Create spreadsheet-style business rules with conditions and outcomes. Decision tables let you define business rules in a familiar spreadsheet format. Each row is a rule: conditions on the left, outcomes on the right. ## Creating a decision table 1. Open your decision graph 2. Drag a **Decision Table** node onto the canvas 3. Connect it to your Input node (or other upstream nodes) 4. Click **Edit Table** to open the editor ## Hit policies Hit policies control what happens when multiple rows match. | Policy | Behavior | | ----------- | ---------------------------------------- | | **First** | Returns the first matching row (default) | | **Collect** | Returns all matching rows as an array | You can also collect a single column while the rest of the table stays first-hit — see [Per-column collect](#per-column-collect). To change the hit policy, click on the **Settings** on the decision graph node. ### First hit (default) The engine evaluates rows top to bottom and stops at the first match. Order your rows from most specific to most general: ### Collect Returns all matching rows. Useful when you need to apply multiple rules: * Calculate all applicable fees * Find all matching promotions * Aggregate scores from multiple criteria ### Per-column collect End an output field with `[]` to collect that column across every matching row, while the remaining columns follow the first-hit policy. The column returns an array with one entry per matching row that has a value in it. For example, a table with output fields `tier` and `tags[]` returns the `tier` from the first matching row, and `tags` as an array gathered from all matching rows: ```json theme={null} { "tier": "gold", "tags": ["loyal-customer", "high-value", "free-shipping"] } ``` Use per-column collect when one table decides a primary outcome and accumulates reasons, tags, or adjustments at the same time. ### When no rows match If no rows match the input: * **First hit policy** - Returns `null` * **Collect policy** - Returns an empty array `[]` To handle this, add a **catch-all row** at the bottom with empty conditions that matches any input: The last row with empty conditions acts as a default, ensuring you always get a meaningful result. ## Adding columns Decision tables have two column types: **Input columns** - Define conditions to match against incoming data **Output columns** - Define values to return when conditions match To add a column: 1. Click **+** in the header row 2. Select **Input** or **Output** 3. Enter the field path (e.g., `customer.tier` or `order.total`) 4. Give it a readable label ## Input column types Input columns can be configured in two modes: ### Targeted field (unary) The default mode. Configure a field path (like `customer.revenue`) in the column settings, then write simple conditions in each cell: The field path is evaluated automatically - you only write the comparison operator and value. ### Generic field (standard) Set the field to `-` (empty) to write full expressions in each cell: Use generic columns when you need to: * Compare multiple fields in one condition * Write complex expressions that don't fit the unary pattern * Reference previous nodes with `$nodes` ## Writing conditions Input columns use **unary test syntax** - shorthand expressions evaluated against each cell's value. ### Condition syntax | Type | Syntax | Example | Matches | | ----------------- | -------------------------------- | ------------------ | ------------------------------------ | | Comparison | `>`, `<`, `>=`, `<=`, `==`, `!=` | `>= 100` | Values 100 or greater | | Range (inclusive) | `[min..max]` | `[18..65]` | Values from 18 to 65 | | Range (exclusive) | `(min..max)` | `(0..100)` | Values between 0 and 100 | | List | `'a', 'b', 'c'` | `'US', 'CA', 'GB'` | Any listed value | | Combined | `and`, `or` | `> 10 and < 50` | Values matching both | | With functions | `$` | `len($) > 5` | Use `$` to reference the field value | | Any value | *(empty)* | | Matches everything | When you use `$` in a targeted field column, the expression is treated as a standard expression. This lets you use functions like `len($)`, `contains($, 'text')`, or `upper($) == 'VALUE'`. ## Writing outputs Output columns contain the values returned when a row matches. You can use: * **Literal values** - `100`, `"approved"`, `true` * **Expressions** - `input.amount * 0.1` * **References** - `customer.defaultRate` ## Referencing previous nodes Use `$nodes` to access output from upstream nodes in your graph. ### In conditions (generic columns) ### In outputs This lets you build multi-stage decisions where each table can use results from earlier nodes. ## Testing your table 1. Click **Open Simulator** in the toolbar 2. Enter test input as JSON 3. Click **Run** 4. View the matched row and output The simulator highlights which row matched and shows the trace through your decision. ## Best practices **Order rows by specificity** - Put specific conditions before general ones when using first-hit policy. **Use meaningful labels** - Column labels appear in the UI and help others understand your rules. **Add a catch-all row** - End with a row using empty cells in all inputs to handle unexpected cases. **Keep tables focused** - If a table grows beyond 20-30 rows, consider splitting it into multiple tables or using a switch node. # Writing expressions Source: https://docs.gorules.io/learn/authoring/expressions Transform and calculate data using the ZEN expression language. Expression nodes transform data using the ZEN expression language. Use them to calculate values, reshape data, and prepare outputs. ## Creating an expression node 1. Drag an **Expression** node onto the canvas 2. Connect it to your data flow 3. Click **Edit Expression** to open the editor 4. Define output fields and their expressions ## Expression structure Each expression node produces an output object. You define fields and the expressions that calculate them: ## Accessing data Reference input data using dot notation: ``` customer.name // Nested object access order.items[0].price // Array indexing ``` ### Referencing previous nodes Use `$nodes` to access output from any upstream node by its name: ``` $nodes.CreditCheck.rating // Output from "CreditCheck" node $nodes.RiskScore.value // Output from "RiskScore" node $nodes["My Node"].field // Use brackets for names with spaces ``` See [Useful patterns](/learn/authoring/patterns#referencing-previous-nodes-with-nodes) for more examples. ## Operators ### Arithmetic ``` price * quantity // Multiplication total / count // Division base + bonus // Addition gross - deductions // Subtraction amount % 100 // Modulo (remainder) 2 ^ 10 // Power (1024) ``` ### Comparison ``` age >= 18 // Greater than or equal status == "active" // Equal tier != "basic" // Not equal score < threshold // Less than ``` ### Logical ``` isActive and hasPermission // Both must be true isAdmin or isOwner // Either can be true not isBlocked // Negation ``` ### Ternary (conditional) ``` score > 70 ? "pass" : "fail" age >= 18 ? "adult" : age >= 13 ? "teen" : "child" ``` ### Null handling ``` user.nickname ?? user.name ?? "Anonymous" // First non-null value ``` ### Range checks ``` age in [18..65] // Inclusive range score in (0..100) // Exclusive range value not in [1..10] // Outside range ``` ## Built-in functions ### Math | Function | Example | Result | | ---------- | ------------------- | ------ | | `abs(n)` | `abs(-5)` | `5` | | `round(n)` | `round(3.7)` | `4` | | `floor(n)` | `floor(3.9)` | `3` | | `ceil(n)` | `ceil(3.1)` | `4` | | `min(arr)` | `min([3, 1, 4])` | `1` | | `max(arr)` | `max([3, 1, 4])` | `4` | | `sum(arr)` | `sum([1, 2, 3])` | `6` | | `avg(arr)` | `avg([10, 20, 30])` | `20` | ### String | Function | Example | Result | | ----------------------- | --------------------------- | --------------- | | `len(s)` | `len("hello")` | `5` | | `upper(s)` | `upper("hello")` | `"HELLO"` | | `lower(s)` | `lower("HELLO")` | `"hello"` | | `trim(s)` | `trim(" hi ")` | `"hi"` | | `contains(s, sub)` | `contains("hello", "ell")` | `true` | | `startsWith(s, prefix)` | `startsWith("hello", "he")` | `true` | | `split(s, delim)` | `split("a,b,c", ",")` | `["a","b","c"]` | ### Array | Function | Example | Result | | -------------------- | ----------------------------- | ----------- | | `len(arr)` | `len([1, 2, 3])` | `3` | | `map(arr, expr)` | `map([1, 2, 3], # * 2)` | `[2, 4, 6]` | | `filter(arr, expr)` | `filter([1, 2, 3, 4], # > 2)` | `[3, 4]` | | `some(arr, expr)` | `some([1, 2, 3], # > 2)` | `true` | | `all(arr, expr)` | `all([1, 2, 3], # > 0)` | `true` | | `flatMap(arr, expr)` | `flatMap([[1,2], [3]], #)` | `[1,2,3]` | The `#` symbol represents each element when iterating over arrays. ### Date | Function | Example | Result | | -------------------- | ------------------------------------------- | ------------ | | `d(str)` | `d("2024-01-15")` | Date object | | `d().year()` | `d("2024-01-15").year()` | `2024` | | `d().month()` | `d("2024-01-15").month()` | `1` | | `d().day()` | `d("2024-01-15").day()` | `15` | | `d().add(n, unit)` | `d("2024-01-15").add(7, "d")` | 7 days later | | `d().diff(d2, unit)` | `d("2024-01-15").diff("2024-01-01", "day")` | `14` | ## Common patterns ### Calculate totals ### Categorize values ### Work with dates # Function nodes Source: https://docs.gorules.io/learn/authoring/function-nodes Write typed TypeScript for complex logic, API calls, and advanced data processing. Function nodes let you write TypeScript for logic that expressions and decision tables can't handle. Use them for complex algorithms, external API calls, or custom data transformations. ## Creating a function node 1. Add a **Function** node to your decision graph 2. Connect it between other nodes 3. Open the node to edit its code ## Basic structure Every function node exports a typed handler that receives input and returns output: ```typescript theme={null} export const handler = async (input: FunctionInput) => { return input; }; ``` `FunctionInput` is generated for you from the graph's resolved schema at this point in the flow - the same types you see in the [Interface tab](/brms/build/graphs#interface). You don't declare it; it's already in scope. ## Type checking The editor type-checks your code in strict mode as you type. Reference a field that doesn't exist on the input, or use a number where a string is expected, and you get a diagnostic in place - before anything runs. When you change the graph's input schema, `FunctionInput` updates and any code that no longer matches is flagged. Type annotations are optional: a plain JavaScript handler is valid and runs the same. The types are there to catch mistakes, not to gate you. ## Accessing previous nodes Use `input.$nodes` to access outputs from any upstream node in your graph: ```typescript theme={null} export const handler = async (input: FunctionInput) => { // Access output from specific nodes by name const creditScore = input.$nodes.CreditCheck.score; const riskLevel = input.$nodes.RiskAssessment.level; return { approved: creditScore > 700 && riskLevel === "low" }; }; ``` Use `input.$nodes.NodeName.field` to reference any field from an upstream node's output. Node names are case-sensitive and must match exactly. ## Example: Calculate loyalty points ```typescript theme={null} export const handler = async (input: FunctionInput) => { const { orderTotal, customerTier } = input; // Base points: $1 = 1 point let points = Math.floor(orderTotal); // Tier multipliers const multipliers: Record = { platinum: 3, gold: 2, silver: 1.5, bronze: 1 }; const multiplier = multipliers[customerTier] ?? 1; points = Math.floor(points * multiplier); return { loyaltyPoints: points, tier: customerTier, multiplier }; }; ``` ## Supported libraries Function nodes include several built-in libraries: | Library | Purpose | Example | | -------- | ------------------------------ | --------------------------------------- | | `dayjs` | Date and time manipulation | `dayjs().add(7, 'days')` | | `big.js` | Arbitrary-precision decimals | `new Big('0.1').plus('0.2')` | | `zod` | Input validation and parsing | `z.string().email().parse(input)` | | `http` | HTTP requests (axios-like API) | `await http.get(url)` | | `zen` | Evaluate other decisions | `await zen.evaluate(decisionId, input)` | ### Using dayjs ```typescript theme={null} import dayjs from 'dayjs'; export const handler = async (input: FunctionInput) => { const today = dayjs(); const birthDate = dayjs(input.birthDate); const age = today.diff(birthDate, 'year'); return { age, isAdult: age >= 18, nextBirthday: birthDate.add(age + 1, 'year').format('YYYY-MM-DD') }; }; ``` ### Using big.js for precision ```typescript theme={null} import Big from 'big.js'; export const handler = async (input: FunctionInput) => { const price = new Big(input.price); const taxRate = new Big(input.taxRate); const tax = price.times(taxRate); const total = price.plus(tax); return { subtotal: price.toNumber(), tax: tax.toNumber(), total: total.toNumber() }; }; ``` ### Making HTTP requests ```typescript theme={null} import http from 'http'; export const handler = async (input: FunctionInput) => { const response = await http.get('https://api.example.com/rates', { params: { currency: input.currency } }); return { exchangeRate: response.data.rate, updatedAt: response.data.timestamp }; }; ``` ## Async/await Function nodes support async operations. Use `async/await` for any asynchronous logic: ```typescript theme={null} export const handler = async (input: FunctionInput) => { // Parallel API calls const [userData, orderHistory] = await Promise.all([ fetchUser(input.userId), fetchOrders(input.userId) ]); return { user: userData, totalOrders: orderHistory.length, lifetimeValue: orderHistory.reduce((sum, o) => sum + o.total, 0) }; }; ``` ## Debugging Use `console.log` to debug your functions. Output appears in the function editor's console and in simulation traces: ```typescript theme={null} export const handler = async (input: FunctionInput) => { console.log('Input received:', input); const result = calculateSomething(input); console.log('Calculation result:', result); return result; }; ``` ## Execution limits Function nodes have these constraints: | Limit | Value | | ----------------- | ------------------- | | Execution timeout | 5000ms (by default) | | Memory | Shared with engine | If your function exceeds the timeout, evaluation fails with an error. ## Best practices **Keep functions focused** - Each function should do one thing well. Use multiple function nodes for complex workflows. **Handle errors gracefully** - Wrap risky operations in try/catch blocks. **Avoid side effects** - Functions should be deterministic when possible. Same input should produce same output. **Use expressions first** - If the ZEN expression language can handle your logic, prefer it over function nodes for better performance. ```typescript theme={null} // Handle potential errors export const handler = async (input: FunctionInput) => { try { const result = await riskyOperation(input); return { success: true, data: result }; } catch (error) { console.error('Operation failed:', error.message); return { success: false, error: error.message }; } }; ``` # Natural language Source: https://docs.gorules.io/learn/authoring/natural-language Read and edit rules as plain sentences - the same logic, rendered for business users. Every rule in GoRules has two faces: the ZEN expression a developer writes, and a natural-language sentence anyone can read. They are the same stored logic - natural language is a display mode, not a translation step, so nothing can drift out of sync. ``` applicant.employment in ["EMPLOYED", "SELF_EMPLOYED"] ``` renders as: > applicant.employment **is one of** Employed, Self-employed ## Switching modes The **Developer mode** switch in the editor header controls the default for everything you open: business mode shows sentences, developer mode shows expressions. You can also flip an individual rule with the **Rule display: Code | Natural** toggle. The stored rule never changes - only how it's shown to you. ## How expressions become sentences Operators map to phrases, and the phrasing adapts to the data type: | Expression | Reads as | | ------------------------------ | ------------------------------------------- | | `score >= 640` | score **is at least** 640 | | `startDate >= d('2026-01-01')` | startDate **is on or after** 1 January 2026 | | `country in ["US", "CA"]` | country **is one of** US, CA | | `amount in [100..500]` | amount **is between** 100 **and** 500 | | `tags contains "vip"` | tags **contains** vip | | `sum(items.price)` | **the sum of** items.price | | `fee ?? 0` | fee **or else** 0 | Note the date-awareness: `>=` reads "is at least" for numbers but "is on or after" for dates. Aggregations read as phrases too - "the average of", "the smallest of", "the number of items in". ## Dictionary labels When a field is typed with a [dictionary](/learn/authoring/policies#dictionaries-shared-vocabulary), sentences use the labels, not the stored values: `terms in ["NET30", "NET60"]` reads "terms **is one of** Net 30 days, Net 60 days". Business users see the vocabulary they know; evaluation still compares the underlying values. ## Editing in natural language Natural language is editable, not just readable: * Values in a sentence are tokens you can click and change in place. Date and time tokens open a calendar and time picker, so "is on or after *1 March 2026*" is edited by picking a date, not by typing an ISO string. * Dictionary-typed tokens offer their labelled options. * If you rewrite a sentence in your own words, GoRules AI converts it back into a valid expression - and shows you the result, so the logic stays precise. ## When to use which mode There's no wrong answer - the modes exist so each person works in the notation they're fastest in. Analysts review and edit rules as sentences; developers drop to code for complex expressions; both look at the same rules. A practical default: author precise logic in developer mode, review and demo in business mode. Natural language is only as readable as your vocabulary. Dictionaries and well-named fields do most of the work - "employment is one of Employed, Self-employed" reads well because the labels exist. # Patterns and techniques Source: https://docs.gorules.io/learn/authoring/patterns Essential patterns for building decision graphs: data flow control, multi-stage decisions, array processing, and validation. This guide covers patterns you'll use regularly when building decision graphs. Start with the fundamentals (pass-through, `$nodes`, output organization) before moving to array processing and validation. *** ## Fundamentals These patterns apply to most decision graphs. ### Data flow with passThrough By default, **passThrough is enabled** - each node carries forward all previous data plus its own outputs. Disable it when you want to return only the node's output fields. In the editor, the **→** icon on a node indicates passThrough is enabled. **With passThrough (default):** ``` Input: { customer: { tier: "gold" }, order: { total: 150 } } ↓ Decision Table outputs: { discount: 0.15 } ↓ Final output: { customer: { tier: "gold" }, order: { total: 150 }, discount: 0.15 } ``` **Without passThrough:** ``` Input: { customer: { tier: "gold" }, order: { total: 150 } } ↓ Decision Table outputs: { discount: 0.15 } ↓ Final output: { discount: 0.15 } // Only this node's output ``` Disable passThrough when: * You want to return only the calculated results (common for final nodes) * You need to reshape the output structure completely ### Self-reference with \$ In expression nodes, use `$` to reference values calculated earlier in the same node: ``` subtotal = sum(map(items, #.price * #.quantity)) tax = $.subtotal * 0.08 shipping = $.subtotal > 100 ? 0 : 9.99 total = $.subtotal + $.tax + $.shipping ``` Each line can reference values from previous lines using `$.fieldName`. ### Referencing previous nodes with \$nodes Use `$nodes` to access output from any upstream node in your graph. Each node's output is available by its name. **In expressions:** ``` $nodes.CreditScore.rating // Output from a node named "CreditScore" $nodes.IncomeCheck.level // Output from a node named "IncomeCheck" $nodes.RiskAssessment.score // Output from a node named "RiskAssessment" ``` **In function nodes (JavaScript):** ```javascript theme={null} export const handler = async (input) => { const creditRating = input.$nodes.CreditScore.rating; const incomeLevel = input.$nodes.IncomeCheck.level; return { eligible: creditRating === "good" && incomeLevel === "sufficient" }; }; ``` **In decision tables:** You can reference `$nodes` in both input conditions and output expressions: | Customer Tier | Credit Rating | → | Discount | | ------------- | ---------------------------------------------------- | - | -------- | | `'gold'` | `$nodes.CreditCheck.rating == 'excellent'` | | `0.20` | | `'silver'` | `$nodes.CreditCheck.rating in ['good', 'excellent']` | | `0.10` | Node names are case-sensitive and must match exactly. If your node is named "Credit Score" with a space, reference it as `$nodes["Credit Score"].field`. ### Organizing output with outputPath Use **outputPath** to structure your output into nested objects instead of flat fields. **Without outputPath** - all outputs merge at root level: ```json theme={null} { "isEligible": true, "code": "eligible", "responsibleParty": "seller", "refundAmount": 89.99 } ``` **With outputPath** - organize related fields into groups: | Node | outputPath | Fields | | ----------------- | -------------- | ------------------------------------ | | Eligibility check | `returnStatus` | `isEligible`, `code`, `message` | | Responsibility | `resolution` | `responsibleParty`, `refundApproved` | Result: ```json theme={null} { "returnStatus": { "isEligible": true, "code": "eligible", "message": "Return within policy" }, "resolution": { "responsibleParty": "seller", "refundApproved": true } } ``` You can also use dot notation in output field definitions like `resolution.responsibleParty` to achieve the same structure. *** ## Array and collection patterns These patterns handle lists of items and multiple matching rules. ### Processing arrays with loop mode When your input contains an array of items that each need evaluation, use **loop execution mode**. The node processes each array element individually and collects the results. **Configuration:** | Property | Description | | --------------- | ----------------------------------------------------------- | | `executionMode` | Set to `loop` to iterate over an array | | `inputField` | Path to the array to process (e.g., `testResults`, `items`) | | `outputPath` | Where to store the results array | Loop mode outputs an array at the root level. Without `outputPath`, you'd get unusable output like `[{ flag: "critical" }, { flag: "abnormal" }]`. Always specify `outputPath` to place results in a named field. **Example: Lab results interpreter** Input data: ```json theme={null} { "testResults": [ { "testType": "glucose", "value": 260 }, { "testType": "potassium", "value": 3.2 }, { "testType": "hemoglobin", "value": 10.2 } ] } ``` Decision table configuration: * `executionMode`: `loop` * `inputField`: `testResults` * `outputPath`: `testResults` * `passThrough`: `true` The table evaluates each test result individually: | Value | Test Type | → | Flag | Condition | | ------- | -------------- | - | ------------ | ----------------- | | `< 3.5` | `'potassium'` | | `'critical'` | `'Hypokalemia'` | | `> 200` | `'glucose'` | | `'abnormal'` | `'Hyperglycemia'` | | `< 8.5` | `'hemoglobin'` | | `'abnormal'` | `'Anemia'` | Output: ```json theme={null} { "testResults": [ { "testType": "glucose", "value": 260, "flag": "abnormal", "condition": "Hyperglycemia" }, { "testType": "potassium", "value": 3.2, "flag": "critical", "condition": "Hypokalemia" }, { "testType": "hemoglobin", "value": 10.2, "flag": null, "condition": null } ] } ``` ### Collecting multiple matches When multiple rules can apply to a single input, use **collect hit policy** to return all matching rows as an array. | Hit Policy | Behavior | Use when | | ---------------------------- | -------------------------------------------- | ------------------------------------------- | | `first` | Returns first matching row | Rules are mutually exclusive | | `collect` | Returns all matching rows as array | Multiple rules can apply | | `first` + `[]` output column | Collects one column across all matching rows | One primary outcome plus accumulated values | Collect mode outputs an array at the root level. Use `outputPath` to place results in a named field (e.g., `discounts.safetyFeatures`), otherwise you'll get `[{ percentage: 3 }, { percentage: 2 }]` at root. **Example: Safety feature discounts** A customer's vehicle has multiple safety features. Each feature qualifies for a separate discount: ```json theme={null} { "policy": { "safetyFeatures": ["antiTheftSystem", "dashCam", "advancedDriverAssistance"] } } ``` Decision table with `hitPolicy: collect`: | Safety Features | → | Discount % | Description | | ----------------------------------------- | - | ---------- | ----------------------- | | `contains($, 'antiTheftSystem')` | | `3` | `'Anti-theft discount'` | | `contains($, 'dashCam')` | | `2` | `'Dash cam discount'` | | `contains($, 'advancedDriverAssistance')` | | `5` | `'ADAS discount'` | With `outputPath: discounts.safetyFeatures`, the output becomes: ```json theme={null} { "discounts": { "safetyFeatures": [ { "percentage": 3, "description": "Anti-theft discount" }, { "percentage": 2, "description": "Dash cam discount" }, { "percentage": 5, "description": "ADAS discount" } ] } } ``` Then sum the discounts in an expression node: ``` sum(map(discounts.safetyFeatures, #.percentage)) // Returns 10 ``` **Per-column collect alternative** When you only need to accumulate values (not whole row objects), keep the table on `first` hit policy and end the output field with `[]`. A column with field `discounts.percentages[]` returns `[3, 2, 5]` directly, and the table can still decide first-match outputs in its other columns. This avoids the root-level array and the `outputPath` workaround entirely. *** ## Workflow patterns These patterns control the flow of your decision graph. ### Conditional branching with switch nodes Use **switch nodes** to route data through different paths based on conditions. A switch node has: * **Conditions**: Expressions that determine which path to take * **Handles**: Output connections for each condition * **Default**: Fallback path when no conditions match **Example: Approval workflow** ``` ┌─── approved ───→ [Generate Approval] [Evaluate] → [Switch] └─── rejected ───→ [Generate Rejection] ``` Switch configuration: ``` Condition 1: evaluation.isApproved == true → handle: "approved" Default: → handle: "rejected" ``` Each path can have different downstream nodes that produce different outputs. **Collect mode for switches:** Set `hitPolicy: collect` on a switch node to execute **all matching branches** instead of just the first. Results from all branches are merged. ### Validation pattern Validate input early and branch based on validity. **Step 1: Validation table** - Create a decision table that checks for invalid conditions: | Condition | → | Error | IsValid | | --------------------- | - | --------------------------- | ------- | | `weight <= 0` | | `'Weight must be positive'` | `false` | | `weight > 70` | | `'Exceeds max weight'` | `false` | | `length > 200` | | `'Exceeds max length'` | `false` | | *(empty - catch all)* | | | `true` | **Step 2: Branch on validity** - Use a switch node to route: * Valid requests → continue processing * Invalid requests → return error response directly ``` [Input] → [Validate] → [Switch] ─── valid ───→ [Process] → [Output] └─── invalid ──→ [Output] ``` This pattern prevents wasted processing on invalid data and provides clear error messages. ### Input schema validation Add a JSON Schema to your input node to validate incoming data structure: ```json theme={null} { "type": "object", "properties": { "creditScore": { "type": "number" }, "annualIncome": { "type": "number" }, "employmentStatus": { "type": "string" } }, "required": ["creditScore", "annualIncome"] } ``` Invalid input is rejected before any processing occurs, with a clear error indicating what's wrong. *** ## Putting it all together Real decisions often combine multiple patterns. Here's a loan approval flow: ``` [Input with Schema] ↓ [Credit Score Table] ─── passThrough: true ───→ adds creditRating, creditPoints ↓ [Income Table] ─── passThrough: true ───→ adds incomeLevel, incomePoints ↓ [Calculate DTI] ─── passThrough: true ───→ adds dtiRatio ↓ [Rejection Reasons] ─── hitPolicy: collect, outputPath: rejectionReasons ↓ [Switch] ─── len(rejectionReasons) == 0 ───→ [Calculate Interest Rate] └─── default ───→ [Return Rejection] ``` This flow: 1. Validates input structure via schema 2. Accumulates scores from multiple evaluation tables 3. Collects all applicable rejection reasons 4. Branches based on whether any rejections exist 5. Returns either approval with rate or rejection with reasons # Authoring policies Source: https://docs.gorules.io/learn/authoring/policies Model shared definitions and rules as a document - no nodes or wiring, just blocks the engine orders for you. A policy is the second way to author rules in GoRules. Where a [decision graph](/learn/authoring/decision-graphs) wires nodes into an explicit flow, a policy reads like a document: text and rule blocks mixed together, in whatever order tells the story best. The engine works out execution order itself, from what each block reads and writes. Use a graph when the flow *is* the logic - branching, routing, staged processing. Use a policy when you're defining the facts and vocabulary the rest of your rules build on. ## The mental model Think of a policy as a pure function over your data: an object goes in, and comes back enriched with everything the policy's blocks computed. Each block declares what it produces; any block can use what another block produced. If one block computes `applicant.debtToIncome` and a decision table reads it, the engine runs them in the right order - you never sequence anything by hand. Because order doesn't matter, you can organise the document for readers: headings, explanatory paragraphs, and the rules they describe, side by side. The documentation and the executable logic are the same artifact. ## Block types Type `/` in the editor to insert a block: | Block | What it does | | -------------- | -------------------------------------------------------- | | Dictionary | Names a set of valid values with display labels. | | Data Model | Declares an entity and its typed properties. | | Expression | Computes a value and writes it to a property. | | Decision Table | Spreadsheet-style rules - identical to tables in graphs. | | Match | Picks an outcome from ordered conditions. | | Assertion | States a condition that must hold. | | Globals | Values shared across the whole policy. | Text blocks - headings, paragraphs, lists - carry no logic. Use them generously; a policy that explains itself is the point. ## Dictionaries: shared vocabulary A dictionary turns a set of magic strings into a named, labelled type: | Value | Label | | --------------- | ------------- | | `EMPLOYED` | Employed | | `SELF_EMPLOYED` | Self-employed | | `RETIRED` | Retired | | `UNEMPLOYED` | Unemployed | Type a data model property as `employmentStatus` and only those values are valid - a typo like `"EMPLYED"` is flagged as you type, not discovered in production. Decision table columns typed with a dictionary offer a labelled dropdown instead of free text, and [natural language mode](/learn/authoring/natural-language) renders the labels in rules: "employment *is one of* Employed, Self-employed". ## Data models: typed entities A data model declares an entity and its properties - `string`, `number`, `boolean`, `date`, or any dictionary; properties can be optional or arrays. Once an entity is declared, every rule that touches it is type-checked, and the entity browser shows each property's type, who writes it, and where it's used. ## Rules that build on each other Expressions, tables, and match blocks write properties; other blocks read them: ``` applicant.debtToIncome = round(applicant.monthlyDebt / applicant.monthlyIncome, 2) ``` Two constraints keep this sound, both enforced by static analysis: only one block may write a given property (`DUPLICATE_WRITER`), and dependencies can't form a cycle (`CYCLIC_DEPENDENCY`). Within those rules, compose freely. ## Imports Policies import other policies, and graphs import policies. An import pulls in the whole chain - importing a policy also brings everything it imports - and the group evaluates together in one shared namespace. That's how a single `lending-policy` can define the dictionaries, entities, and derived facts that every graph in a project relies on. ## Evaluating a policy A policy is directly executable: evaluate it with an input object and it returns the object plus everything computed. In the BRMS every policy is also an HTTP endpoint, exactly like a graph - see [Developer Tools](/developers/developer-tools). ## Where to go next The policy editor, entity browser, and import workflow. How rules render as readable sentences. # Test with simulator Source: https://docs.gorules.io/learn/authoring/testing Validate your rules by running test inputs and tracing execution through each node. The simulator lets you test decisions before deploying them. Run sample inputs, see which rules match, and trace data flow through every node. ## Opening the simulator Click **Open Simulator** in the top-right toolbar. The simulator panel opens at the bottom of the canvas with three sections: * **Events panel** (left) - Manage test events * **Node trace** (center) - Search and inspect node execution * **Results panel** (right) - View Output, Input, and Trace tabs ## Managing test events In the BRMS, test events are organized into: * **Unsaved** - Temporary events that aren't persisted * **Private** - Your personal saved events * **Shared** - Events shared with your team Click **+** to create a new event, or select an existing one to run it. In the playground, you work with a single request JSON. Enter your test data directly in the Input tab. ## Running a test 1. Select or create a test event 2. Enter your test input as JSON 3. Click the **Run** button (play icon) or press `Enter` 4. View results in the Output tab ```json theme={null} { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 250, "items": 5 } } ``` ## Reading results After running a test, you see: **Output** - The final result returned by your decision **Trace** - Step-by-step execution showing: * Which nodes executed * What data each node received * What each node produced * Which decision table rows matched ## Trace view The trace shows execution order and data at each step: | Step | Node | Input | Output | | ---- | ---------------- | ---------------------------------- | --------------------------------- | | 1 | Input | - | `{customer: {...}, order: {...}}` | | 2 | Calculate Totals | `{order: {...}}` | `{subtotal: 250, tax: 20}` | | 3 | Discount Rules | `{customer: {...}, subtotal: 250}` | `{discount: 0.15}` | | 4 | Output | `{discount: 0.15, ...}` | Final result | Click any node in the trace to see its full input and output data. ## Decision table tracing For decision tables, the trace shows: * **Evaluated rows** - All rows that were checked * **Matched row** - The row (or rows) that matched * **Match details** - Which conditions passed or failed This helps you understand why a particular row matched or didn't match. ## Performance metrics The trace includes timing for each node: * **Execution time** - Microseconds spent in each node * **Total time** - End-to-end evaluation time Use these metrics to identify slow nodes in complex decisions. ## Testing strategies ### Test edge cases Create inputs that test boundary conditions: ```json theme={null} // Test the boundary at exactly 100 { "order": { "total": 100 } } // Test just below the boundary { "order": { "total": 99.99 } } // Test just above the boundary { "order": { "total": 100.01 } } ``` ### Test each decision table row Create inputs designed to match each row in your tables. This ensures all paths work correctly. ### Test error conditions Try inputs with: * Missing fields * Null values * Invalid data types * Empty arrays ### Save test events In the BRMS, save your test events by clicking the menu icon next to the event and selecting **Save to Private**. Build a library of events that cover your critical scenarios. ## Debugging tips **No output?** Check that all nodes are connected. Data can't flow through disconnected nodes. **Wrong row matched?** Review your decision table row order. With first-hit policy, earlier rows take precedence. **Unexpected null?** Trace the data to find where the value became null. Check for typos in field names. **Expression error?** The trace shows the exact expression that failed and the error message. # Create your first rule Source: https://docs.gorules.io/learn/getting-started/first-rule Build and test a discount pricing rule in 5 minutes using the GoRules visual editor. In this quickstart, you'll create a decision that calculates customer discounts based on order value and membership tier. ## What you'll build A pricing rule that: * Gives Gold members 15% off orders over \$100 * Gives Silver members 10% off orders over \$100 * Gives all customers 5% off orders over \$200 * Returns the best applicable discount ## Step 1: Create a new decision 1. Open your GoRules BRMS instance 2. Navigate to your project (or create one) 3. Click on **Open Editor** 4. Click **New** and select **File** 5. Name it "Customer Discount" and press Enter The visual editor opens with an empty canvas. 1. Open the [GoRules Playground](https://editor.gorules.io) 2. You'll see an empty canvas No account needed - the playground runs entirely in your browser. ## Step 2: Add an input node 1. From the node palette, drag an **Input** node onto the canvas 2. This is where your data enters the decision ## Step 3: Add a decision table 1. Drag a **Decision Table** onto the canvas 2. Position it to the right of the Input node 3. Connect the Input node to the Decision Table by dragging from the Input's output handle to the Decision Table's input handle Your graph now looks like: `Input → Decision Table` ## Step 4: Define input columns Click **Edit table** to open the editor. Add two input columns: 1. Click **Add input**. 2. Set the field to `customer.tier` and label it "Customer Tier" 3. Click **Add input** again 4. Set the field to `order.subtotal` and label it "Order Subtotal" ## Step 5: Define output columns Add one output column: 1. Click **Add output** 2. Set the field to `discount` and label it "Discount" ## Step 6: Add your rules Add rows to define the discount logic. For each row, enter the conditions and outcome: Leave a cell empty to match any value. The last row acts as a default that catches all other cases. ## Step 7: Test your rule Click **Open Simulator** in the toolbar. Create a test event and enter: ```json theme={null} { "customer": { "tier": "gold" }, "order": { "subtotal": 150 } } ``` Click **Run**. You should see: ```json theme={null} { "discount": 0.15 } ``` Try different inputs to verify your logic: | Test case | Expected discount | | -------------------------- | ----------------- | | Gold member, \$150 order | 0.15 | | Silver member, \$150 order | 0.10 | | Bronze member, \$250 order | 0.05 | | Bronze member, \$50 order | 0 | ## Step 8: Integrate with your application Now that your rule works, integrate it into your application. The local changes must be committed to `main` branch before evaluation is possible. Call your decision via HTTP from the BRMS: ```javascript Node.js theme={null} const response = await fetch( 'https://[your-brms-url]/api/projects/{projectId}/evaluate/{documentPath}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Access-Token': '[your-project-token]' }, body: JSON.stringify({ context: { customer: { tier: 'gold' }, order: { subtotal: 150 } } }) } ); const data = await response.json(); console.log(data.result); // => { discount: 0.15 } ``` Replace `{projectId}` with your project ID and `{documentPath}` with the decision path (e.g., `customer-discount`). Run rules locally by embedding the ZEN Engine. Export your decision from the BRMS or save it from the playground, then load it with any SDK: # Key concepts Source: https://docs.gorules.io/learn/getting-started/key-concepts Learn the building blocks of GoRules: decision graphs, decision tables, expressions, and functions. GoRules uses a visual decision graph model. Understanding these building blocks helps you work effectively with the platform. ## Decision graph A decision graph is a visual canvas where you connect nodes to model decision logic. Data flows from an input node, through processing nodes, to an output node. Every decision graph has: * **Input node** - Receives the data you want to evaluate * **Processing nodes** - Transform and evaluate the data (decision tables, expressions, functions, switches) * **Output node** - Returns the final result (optional unless you need output validation) You build graphs by dragging nodes onto the canvas and connecting them. Data flows through the connections, with each node receiving input from the previous node and passing output to the next. ## Decision table A decision table is a spreadsheet-like component for conditional logic. Rows define rules: conditions on the left, outcomes on the right. The engine evaluates rows top-to-bottom. By default, it returns the first matching row (first-hit policy). You can also collect all matching rows when needed. **When to use:** Conditional logic with multiple rules, lookup tables, classification, eligibility checks. ### Unary tests vs standard expressions When an input column has a **field name defined**, cells use unary test syntax - shorthand expressions evaluated against that field: | Operator | Example | Matches | | ----------------- | ------------------------ | ------------------------------- | | Comparison | `> 100`, `<= 50`, `!= 0` | Values matching the comparison | | Range (inclusive) | `[1..10]` | Values from 1 to 10 | | Range (exclusive) | `(0..100)` | Values between 0 and 100 | | List | `'US', 'GB', 'CA'` | Any value in the list | | Combined | `> 5 and < 10` | Values matching both conditions | | Any | *(empty)* | Matches any value | When an input column has **no field name** (empty), cells use standard expressions instead. This lets you write full expressions like `customer.age > 18 and customer.country == 'US'`. ### Hit policies Hit policies control how the engine handles multiple matching rows: | Policy | Behavior | | ----------- | ---------------------------------------- | | **First** | Returns the first matching row (default) | | **Collect** | Returns all matching rows as an array | Output columns whose field ends in `[]` collect their values across all matching rows, even under the First policy. ## Expression node An expression node transforms data using the ZEN expression language. Use it for calculations, mappings, and data manipulation. Use `$` to reference the current expression node's output. In the example above, `$.subtotal` refers to the `subtotal` field calculated earlier in the same node. The operators and functions below work throughout GoRules - in expression nodes, decision table cells, and switch conditions. ### Operators | Type | Operators | | --------------- | ------------------------------------ | | Arithmetic | `+`, `-`, `*`, `/`, `%`, `^` (power) | | Comparison | `==`, `!=`, `>`, `<`, `>=`, `<=` | | Logical | `and`, `or`, `not` | | Ternary | `condition ? then : else` | | Null coalescing | `value ?? fallback` | | Range check | `x in [1..10]`, `x not in (0..100)` | See [Operators](/learn/zen-language/operators) for the complete reference. ### Built-in functions | Category | Functions | | -------- | ----------------------------------------------------------------------------------------- | | Math | `abs`, `floor`, `ceil`, `round`, `min`, `max`, `sum`, `avg`, `median` | | String | `len`, `upper`, `lower`, `trim`, `contains`, `startsWith`, `endsWith`, `matches`, `split` | | Array | `map`, `filter`, `some`, `all`, `one`, `none`, `count`, `flatMap`, `keys`, `values` | | Date | `d()`, `duration` | | Type | `string`, `number`, `bool`, `type`, `isNumeric` | See [Built-in functions](/learn/zen-language/functions) for the complete reference. **When to use:** Calculations, data transformation, mapping values, combining fields. ## Function node A function node runs custom JavaScript for complex logic that expressions can't handle. Use it when you need external API calls, complex algorithms, or operations that require full programming language capabilities. ```javascript theme={null} import dayjs from 'dayjs'; /** @type {Handler} **/ export const handler = async (input) => { const { customerId, orderTotal } = input; // Complex loyalty calculation const loyaltyPoints = Math.floor(orderTotal * 1.5); // Date-based promotions const today = dayjs(); const isWeekend = today.day() === 0 || today.day() === 6; const bonusMultiplier = isWeekend ? 2 : 1; return { loyaltyPoints: loyaltyPoints * bonusMultiplier, earnedOn: today.format('YYYY-MM-DD') }; }; ``` Function nodes support: * **ES6+ JavaScript** - Modern syntax including async/await * **Built-in libraries** - `dayjs` for dates, `big.js` for precision math, `zod` for validation * **Async operations** - Await promises for API calls or async logic **When to use:** Complex algorithms, async operations, logic that requires full JavaScript capabilities. ## Switch node A switch node routes data through different paths based on conditions. It evaluates conditions in order and sends data down the first matching branch. Switch node Each branch leads to its own chain of nodes, allowing you to build parallel processing paths that merge back into a single output. **When to use:** Conditional branching, routing logic, different processing paths based on input values. ## How data flows When you evaluate a decision, data flows through the graph: 1. **Input** - You provide a JSON object with your data 2. **Processing** - Each node receives data, processes it, and passes results forward 3. **Output** - The final node returns the decision result Output node is optional and most often it's not used. Without one, the engine returns the results from all endpoint nodes combined. ### Pass-through behavior By default, nodes use **pass-through mode** - they carry forward all incoming data plus their own outputs. This means downstream nodes can access both the original input and any values added by previous nodes. ``` Input: { customer: { tier: "gold" }, order: { total: 150 } } ↓ Decision Table adds: { discount: 0.15 } ↓ Output: { customer: { tier: "gold" }, order: { total: 150 }, discount: 0.15 } ``` You can disable pass-through on any node when you want to return only that node's output fields. The **→** icon on a node indicates pass-through is enabled. ```javascript theme={null} // Input { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 150, "items": 5 } } // Output (after flowing through the graph) { "discount": 15, "freeShipping": true, "loyaltyBonus": 225 } ``` Nodes can access data from: * **Direct input** - Data passed directly into the node * **Previous nodes** - Output from upstream nodes in the graph ### Referencing previous nodes Use `$nodes` to access the output of any upstream node by its name: ``` $nodes.DiscountTable.discount // Output from a node named "DiscountTable" $nodes.RiskScore.level // Output from a node named "RiskScore" $nodes["My Node"].value // Use brackets for names with spaces ``` This lets you build multi-stage decisions where later nodes use results from earlier ones. **Example: Multi-stage loan decision** ``` [Input] → [Credit Score] → [Income Check] → [Final Decision] ``` The Credit Score node outputs `{ rating: "good", score: 720 }`. The Income Check node outputs `{ sufficient: true }`. In the Final Decision expression node, you can combine both: ``` approved = $nodes.CreditScore.rating in ['good', 'excellent'] and $nodes.IncomeCheck.sufficient ``` In function nodes, access `$nodes` through the input parameter: ```javascript theme={null} const creditRating = input.$nodes.CreditScore.rating; const incomeOk = input.$nodes.IncomeCheck.sufficient; ``` ### Special symbols The `$` symbol has different meanings depending on context: | Symbol | Context | Meaning | | -------- | --------------------------- | ------------------------------------------------------- | | `$` | Expression node | Reference to current node's output (e.g., `$.subtotal`) | | `$` | Unary test (decision table) | The field value being tested (e.g., `len($) > 5`) | | `$nodes` | Any node | Access output from previous nodes | | `$root` | Expression node | The entire context object | | `#` | Array iteration | Current element in `map`, `filter`, etc. | ## JDM file format Decision graphs are stored as JSON Decision Model (JDM) files. This portable format lets you: * Version control rules in Git * Move rules between environments * Share rules across applications * Edit rules programmatically You don't need to understand the JDM format to use GoRules - the visual editor handles it automatically. But if you want to work with rules programmatically, the format is documented in the [JDM Format section](/learn/jdm-format/standard). # Interactive playground Source: https://docs.gorules.io/learn/getting-started/playground Experiment with the GoRules visual editor directly in your browser - no installation required. The GoRules Playground lets you build, test, and export decision models without creating an account or installing anything. Launch the visual editor in your browser ## What you can do The playground includes the full visual editor: * **Decision tables** - Create spreadsheet-style conditional logic * **Expression nodes** - Transform data with the ZEN expression language * **Function nodes** - Write custom JavaScript for complex logic * **Switch nodes** - Route data through conditional branches * **Simulator** - Test your rules with sample data and see step-by-step execution ## Getting started 1. **Open the playground** - Visit [editor.gorules.io](https://editor.gorules.io) 2. **Build your graph** - Drag nodes onto the canvas and connect them 3. **Add logic** - Double-click nodes to configure rules, expressions, or code 4. **Test** - Click **Open Simulator**, enter test data, and run your decision 5. **Export** - Download the JDM file to use with GoRules SDKs ## Try these examples ### Pricing calculator Build a decision that applies discounts based on customer tier and order value: 1. Add a **Decision Table** node 2. Configure inputs: `customer.tier`, `order.total` 3. Configure output: `discount` 4. Add rules for different scenarios ### Eligibility checker Create a decision that determines if a user qualifies for a service: 1. Add an **Expression** node to calculate derived values (age from birthdate, etc.) 2. Add a **Decision Table** to check eligibility criteria 3. Add another **Expression** to format the response ### Risk scoring Build a decision that calculates a risk score from multiple factors: 1. Add multiple **Decision Table** nodes for different risk categories 2. Add an **Expression** node to combine scores 3. Add a **Switch** node to route to different outcomes based on total score ## Exporting your work Click **Export** to download your decision as a JSON file. This JDM (JSON Decision Model) file works with any GoRules SDK: ## Limitations The playground is designed for experimentation. For production use: | Feature | Playground | BRMS | | ---------------------- | ----------- | --------- | | Save decisions | Export only | Automatic | | Version control | No | Yes | | Team collaboration | No | Yes | | Environment management | No | Yes | | Access control | No | Yes | ## Embedding the editor The visual editor is available as an open-source React component. You can embed it in your own applications: ```bash theme={null} npm install @gorules/jdm-editor ``` ```jsx theme={null} import { useState } from 'react'; import { DecisionGraph, JdmConfigProvider } from '@gorules/jdm-editor'; import '@gorules/jdm-editor/dist/style.css'; function RuleEditor() { const [graph, setGraph] = useState({ nodes: [], edges: [] }); return ( ); } ``` See the [JDM Editor documentation](/developers/jdm/jdm-editor) for configuration options, simulation support, and theming. # What is GoRules? Source: https://docs.gorules.io/learn/getting-started/what-is-gorules A 2-minute overview of GoRules, the business rules engine that separates decision logic from code. GoRules is a business rules engine that separates decision logic from your application code. You define rules visually or programmatically, test them instantly, and deploy without rewriting software. ## The problem GoRules solves Business logic changes constantly. Pricing tiers, eligibility criteria, approval workflows, and risk scores all evolve with your business. When this logic lives in application code, two things happen: 1. **Developers become the bottleneck**: Business teams know exactly what they want, but every change requires a developer to translate it. 2. **The translation is inelegant**: A pricing matrix that's simple in a spreadsheet becomes 100 nested if-statements in code - brittle, hard to read, and terrifying to modify. GoRules lets business teams own the rules they're experts on, in formats that actually match how they think - decision tables, not if-else chains. Changes go through your existing review and deployment processes. Developers build the system; business teams manage the logic they know best. ## How it works Use the decision graph canvas to build logic with decision tables, expressions, and custom functions. No code required for rule authoring. Run your rules against sample data in the simulator. See exactly how data flows through each node and debug issues before deployment. Embed the rules engine directly in your application using native SDKs, run it as a microservice, or use the managed cloud platform. Publish new rule versions through your approval workflow. The engine picks up changes automatically - no application redeploy or downtime required. ## Performance The ZEN Engine is written in Rust and compiles to native code for each platform. This architecture delivers: * **1M+ evaluations per second** with the Rust SDK * **Sub-millisecond latency** for individual evaluations * **No per-evaluation fees** - rules run locally, not as API calls ## Who uses GoRules GoRules handles decision logic across industries: | Industry | Common use cases | | ----------------- | -------------------------------------------------------------- | | **Financial** | Credit scoring, fraud detection, loan approval, KYC/AML | | **Insurance** | Underwriting, claims processing, premium calculation | | **Healthcare** | Clinical decisions, triage, dosage calculations | | **Aviation** | Booking personalization, loyalty programs, upgrade eligibility | | **Logistics** | Route optimization, shipping rates, warehouse operations | | **Retail** | Dynamic pricing, promotions, marketplace fees | | **Telco** | Plan configuration, data rollover, roaming policies | | **Public Sector** | Eligibility determination, permit evaluation, fund allocation | # Commission calculations Source: https://docs.gorules.io/learn/tutorials/commissions Build tiered commission structures with quotas, accelerators, and team splits. This guide shows how to build commission rules that handle tiered rates, quota attainment, and complex payout structures. ## What you'll build A commission decision that: * Calculates base commission from tiered rates * Applies accelerators for exceeding quota * Handles multiple product types with different rates * Supports team-based splits Want to skip ahead? Download the completed decision and import it directly. ### Decision flow ## Example: Sales commission ### Input data ```json theme={null} { "rep": { "id": "SR-1234", "role": "account_executive", "quota": 100000 }, "period": { "sales": 125000, "newBusiness": 45000, "renewals": 80000 }, "deal": { "amount": 15000, "type": "new_business", "productLine": "enterprise" } } ``` ## Step 1: Base commission rates Define rates by product and deal type: ## Step 2: Quota attainment tiers Calculate quota percentage and tier: Then apply accelerators based on attainment: ## Step 3: Calculate commission Combine base rate with attainment multiplier: ## Step 4: Role-based caps Apply maximum commission by role: ## Output structure ```json theme={null} { "dealAmount": 15000, "baseRate": 0.12, "baseCommission": 1800, "attainmentPercent": 125, "tier": "accelerator_2", "multiplier": 1.5, "adjustedCommission": 2700, "finalCommission": 2700, "capped": false } ``` ## Variations ### Team splits Split commission between reps: ### Spiffs and bonuses Add special incentives: ### Clawbacks Handle commission recovery: ## Best practices **Version commission plans** - Keep history when plans change mid-year. **Show calculations** - Output intermediate values for transparency. **Handle edge cases** - Zero quota, partial periods, mid-month starts. **Audit trail** - Log all inputs and outputs for disputes. # Dynamic pricing rules Source: https://docs.gorules.io/learn/tutorials/dynamic-pricing Build pricing rules that adjust based on customer segments, order values, and promotions. This guide walks through building a dynamic pricing system that calculates discounts, applies promotions, and determines final prices based on customer and order data. ## What you'll build A pricing decision that: * Applies tiered discounts based on customer loyalty level * Adds volume discounts for large orders * Stacks promotional codes * Calculates final price with all adjustments Want to skip ahead? Download the completed decision and import it directly. ### Decision flow ## Example input ```json theme={null} { "customer": { "tier": "gold", "yearsActive": 3 }, "order": { "subtotal": 450, "itemCount": 12 }, "promoCode": "SUMMER20" } ``` ## Step 1: Customer tier discounts Create a decision table for base discounts by customer tier: **Logic:** * Platinum customers always get 20% * Gold customers get 15% after 3 years, 12% before * Silver customers get 10% after 2 years, 8% before * Everyone else gets 5% ## Step 2: Volume discounts Add another decision table for quantity-based discounts: ## Step 3: Promotional codes Handle promo codes with another table: ## Step 4: Calculate final price Use an expression node to combine all discounts: **Key logic:** * Calculate savings from each discount type * Cap total discount at 40% to protect margins * Round to 2 decimal places ## Testing scenarios | Scenario | Expected result | | ------------------------------------------- | ------------------------------ | | Gold customer, 3+ years, 12 items, no promo | 15% tier + 5% volume = 20% off | | New customer, 5 items, NEWUSER code | 5% tier + 15% promo = 20% off | | Platinum, 50 items, SUMMER20 | Capped at 40% (would be 50%) | ## Output structure ```json theme={null} { "baseAmount": 450, "tierSavings": 67.50, "volumeSavings": 22.50, "promoSavings": 90, "totalDiscount": 180, "finalPrice": 270 } ``` ## Variations ### Time-based pricing Add date conditions for seasonal pricing: ### Geographic pricing Adjust by region: # Eligibility and approval workflows Source: https://docs.gorules.io/learn/tutorials/eligibility Build rules that determine qualification for loans, services, benefits, or access. This guide shows how to build eligibility rules that evaluate applicants against multiple criteria and route them through approval workflows. ## What you'll build An eligibility decision that: * Evaluates applicants against qualification criteria * Handles multiple approval paths (auto-approve, manual review, deny) * Provides clear reasons for decisions * Supports tiered approval thresholds Want to skip ahead? Download the completed decision and import it directly. ### Decision flow ## Example: Loan pre-qualification ### Input data ```json theme={null} { "applicant": { "age": 32, "income": 75000, "employmentYears": 4, "creditScore": 720 }, "loan": { "amount": 25000, "purpose": "home_improvement" } } ``` ## Step 1: Basic eligibility checks First, check hard requirements that must be met: ## Step 2: Credit assessment Evaluate credit score bands: ## Step 3: Calculate loan capacity Use an expression to determine maximum loan amount: ## Step 4: Determine approval path Route to different outcomes based on risk: ## Output structure ```json theme={null} { "decision": "approved", "nextStep": "verification", "approvedAmount": 25000, "creditTier": "good", "reasons": ["Meets basic requirements", "Good credit standing"] } ``` ## Example: Benefits eligibility ### Household income check ## Example: Access control ### Feature access by subscription ## Best practices **Fail fast** - Put disqualifying checks first to exit early. **Provide reasons** - Always output why a decision was made. **Handle edge cases** - Include catch-all rows for unexpected inputs. **Version your rules** - Track changes when eligibility criteria update. # Insurance underwriting Source: https://docs.gorules.io/learn/tutorials/insurance Build underwriting rules for risk assessment, premium calculation, and policy decisions. This guide shows how to build insurance underwriting rules that assess risk, calculate premiums, and determine policy terms. ## What you'll build An underwriting decision that: * Evaluates applicant risk factors * Calculates risk-adjusted premiums * Determines coverage eligibility * Applies regulatory constraints Want to skip ahead? Download the completed decision and import it directly. ### Decision flow ## Example: Auto insurance quote ### Input data ```json theme={null} { "applicant": { "age": 28, "yearsLicensed": 10, "accidentsLast5Years": 0, "violationsLast3Years": 1, "creditTier": "good" }, "vehicle": { "year": 2022, "make": "Toyota", "model": "Camry", "value": 28000, "safetyRating": 5, "antiTheft": true }, "coverage": { "type": "full", "deductible": 500, "state": "CA" } } ``` ## Step 1: Driver risk assessment Score driver risk factors: ## Step 2: Driving history Assess accidents and violations: ## Step 3: Vehicle risk Assess vehicle factors: ## Step 4: Calculate base premium ## Step 5: Coverage and deductible adjustments ## Step 6: Credit-based adjustment ## Step 7: Final premium calculation ## Step 8: Eligibility decision ## Output structure ```json theme={null} { "decision": "approve", "annualPremium": 1247.50, "monthlyPremium": 103.96, "riskFactors": { "driver": 1.0, "history": 1.0, "vehicle": 0.9, "credit": 1.0, "combined": 0.9 }, "coverage": { "type": "full", "deductible": 500, "liability": 100000 } } ``` ## Regulatory compliance ### State-specific rules ## Best practices **Audit all decisions** - Log every factor for regulatory review. **Version rating tables** - Track when rates change for policy renewals. **Handle edge cases** - New drivers, antique vehicles, rideshare use. **Test extensively** - Validate against known good quotes. # Risk assessment and scoring Source: https://docs.gorules.io/learn/tutorials/risk-scoring Build scoring models that evaluate risk from multiple weighted factors. This guide shows how to build risk scoring models that combine multiple factors into an overall risk assessment. ## What you'll build A risk scoring decision that: * Evaluates multiple risk factors independently * Weights factors based on importance * Produces a composite risk score * Classifies into risk categories Want to skip ahead? Download the completed decision and import it directly. ### Decision flow ## Example: Fraud risk scoring ### Input data ```json theme={null} { "transaction": { "amount": 1500, "currency": "USD", "country": "NG", "isFirstPurchase": true }, "customer": { "accountAgeDays": 5, "verificationLevel": "email_only", "previousTransactions": 0 }, "device": { "isKnown": false, "vpnDetected": true, "riskScore": 75 } } ``` ## Step 1: Transaction risk factors Score transaction-level signals: ## Step 2: Customer risk factors Evaluate customer signals: ## Step 3: Device risk factors ## Step 4: Calculate composite score Combine all factors with weights: ## Step 5: Risk classification Map score to action: ## Output structure ```json theme={null} { "riskScore": 85, "riskLevel": "critical", "action": "block", "factors": { "transaction": 45, "customer": 65, "device": 50 }, "signals": [ "High-risk country", "New account", "Unknown device", "VPN detected" ] } ``` ## Variations ### Credit risk scoring ## Best practices **Document factor weights** - Make it clear why each factor has its weight. **Normalize scores** - Use consistent 0-100 scales across factors. **Include signal details** - Output which factors contributed to the score. **Tune thresholds** - Adjust classification thresholds based on false positive rates. # Date operations Source: https://docs.gorules.io/learn/zen-language/dates Working with dates and times in ZEN expressions. ZEN provides the `d()` function for comprehensive date handling with timezone support, arithmetic, comparisons, and formatting. ## Creating dates ### From string ``` d("2024-01-15") // 2024-01-15T00:00:00Z d("2024-01-15 14:30") // 2024-01-15T14:30:00Z d("2024-01-15 14:30:45") // 2024-01-15T14:30:45Z ``` ### With timezone ``` d("2024-01-15", "America/New_York") // 2024-01-15T00:00:00-05:00 d("2024-01-15", "Europe/London") // 2024-01-15T00:00:00Z d("2024-01-15 14:30", "Asia/Tokyo") // 2024-01-15T14:30:00+09:00 ``` ### Current date/time ``` d() // Current date and time d("America/Los_Angeles") // Current time in LA timezone ``` ## Date components ### Getters | Method | Example | Result | | -------------- | ----------------------------------- | --------------- | | `.year()` | `d("2024-01-15").year()` | `2024` | | `.month()` | `d("2024-01-15").month()` | `1` | | `.day()` | `d("2024-01-15").day()` | `15` | | `.weekday()` | `d("2024-01-15").weekday()` | `1` (Monday) | | `.hour()` | `d("2024-01-15 14:30").hour()` | `14` | | `.minute()` | `d("2024-01-15 14:30").minute()` | `30` | | `.second()` | `d("2024-01-15 14:30:45").second()` | `45` | | `.dayOfYear()` | `d("2024-01-15").dayOfYear()` | `15` | | `.quarter()` | `d("2024-01-15").quarter()` | `1` | | `.timestamp()` | `d("2024-01-15").timestamp()` | `1705276800000` | ### Setters ``` d("2024-01-15").set("year", 2025) // 2025-01-15 d("2024-01-15").set("month", 6) // 2024-06-15 d("2024-01-15").set("day", 20) // 2024-01-20 ``` ## Date arithmetic ### Adding time ``` d("2024-01-15").add("1d") // 2024-01-16 d("2024-01-15").add("1w") // 2024-01-22 d("2024-01-15").add("1M") // 2024-02-15 d("2024-01-15").add("1y") // 2025-01-15 d("2024-01-15").add("2d 5h") // 2024-01-17T05:00:00 // Alternative syntax d("2024-01-15").add(1, "d") // 2024-01-16 d("2024-01-15").add(3, "M") // 2024-04-15 ``` ### Subtracting time ``` d("2024-01-15").sub("7d") // 2024-01-08 d("2024-01-15").sub("1M") // 2023-12-15 d("2024-01-15").sub(1, "y") // 2023-01-15 ``` ### Duration units | Unit | Aliases | | ------ | ------------------------ | | Year | `y`, `year`, `years` | | Month | `M`, `month`, `months` | | Week | `w`, `week`, `weeks` | | Day | `d`, `day`, `days` | | Hour | `h`, `hour`, `hours` | | Minute | `m`, `minute`, `minutes` | | Second | `s`, `second`, `seconds` | ## Date comparisons ### Comparison methods ``` d("2024-01-15").isBefore(d("2024-01-20")) // true d("2024-01-15").isAfter(d("2024-01-10")) // true d("2024-01-15").isSame(d("2024-01-15")) // true d("2024-01-15").isSameOrBefore(d("2024-01-15")) // true d("2024-01-15").isSameOrAfter(d("2024-01-15")) // true ``` ### Comparison with granularity ``` d("2024-01-15").isSame(d("2024-01-20"), "month") // true (same month) d("2024-01-15").isSame(d("2024-06-15"), "year") // true (same year) d("2024-01-15").isBefore(d("2024-02-01"), "month") // true ``` ### Comparison operators ``` d("2024-01-15") == d("2024-01-15") // true d("2024-01-15") != d("2024-01-20") // true d("2024-01-15") < d("2024-01-20") // true d("2024-01-15") > d("2024-01-10") // true d("2024-01-15") <= d("2024-01-15") // true d("2024-01-15") >= d("2024-01-15") // true ``` ### Range checks ``` d("2024-01-15") in [d("2024-01-01")..d("2024-01-31")] // true d("2024-01-15") in (d("2024-01-01")..d("2024-01-31")) // true d("2024-02-15") not in [d("2024-01-01")..d("2024-01-31")] // true ``` ## Calculating differences ``` d("2024-01-15").diff("2024-01-10", "day") // 5 d("2024-01-15").diff("2024-01-01", "week") // 2 d("2024-06-15").diff("2024-01-15", "month") // 5 d("2025-01-15").diff("2024-01-15", "year") // 1 ``` Negative differences when the first date is earlier: ``` d("2024-01-10").diff("2024-01-15", "day") // -5 ``` ## Start and end of periods ``` d("2024-01-15").startOf("day") // 2024-01-15T00:00:00 d("2024-01-15").endOf("day") // 2024-01-15T23:59:59 d("2024-01-15").startOf("month") // 2024-01-01T00:00:00 d("2024-01-15").endOf("month") // 2024-01-31T23:59:59 d("2024-01-15").startOf("year") // 2024-01-01T00:00:00 d("2024-01-15").endOf("year") // 2024-12-31T23:59:59 d("2024-01-15").startOf("week") // 2024-01-15T00:00:00 (Monday) d("2024-01-15").endOf("week") // 2024-01-21T23:59:59 (Sunday) d("2024-05-15").startOf("quarter") // 2024-04-01T00:00:00 d("2024-05-15").endOf("quarter") // 2024-06-30T23:59:59 ``` ## Timezone conversion ``` d("2024-01-15T12:00:00Z").tz("America/New_York") // 2024-01-15T07:00:00-05:00 d("2024-01-15T12:00:00Z").tz("Europe/London") // 2024-01-15T12:00:00Z d("2024-01-15T12:00:00Z").tz("Asia/Tokyo") // 2024-01-15T21:00:00+09:00 ``` Get timezone name: ``` d("2024-01-15", "America/New_York").offsetName() // "America/New_York" ``` ## Relative dates ``` d().isToday() // true if today d().sub(1, "d").isYesterday() // true d().add(1, "d").isTomorrow() // true ``` ## Validation ``` d("2024-01-15").isValid() // true d("invalid").isValid() // false d("2024-02-30").isValid() // false (invalid date) d(null).isValid() // false ``` ## Leap years ``` d("2024-01-15").isLeapYear() // true d("2023-01-15").isLeapYear() // false d("2000-01-15").isLeapYear() // true d("1900-01-15").isLeapYear() // false ``` ## Formatting ``` d("2024-01-15").format("%Y-%m-%d") // "2024-01-15" d("2024-01-15").format("%Y/%m/%d") // "2024/01/15" d("2024-01-15T14:30:45Z").format("%A, %B %d %Y") // "Monday, January 15 2024" d("2024-01-15T14:30:45Z").format("%H:%M:%S") // "14:30:45" ``` ### Format codes | Code | Description | Example | | ---- | ------------------- | --------- | | `%Y` | 4-digit year | `2024` | | `%m` | Month (01-12) | `01` | | `%d` | Day (01-31) | `15` | | `%H` | Hour (00-23) | `14` | | `%M` | Minute (00-59) | `30` | | `%S` | Second (00-59) | `45` | | `%A` | Full weekday | `Monday` | | `%a` | Abbreviated weekday | `Mon` | | `%B` | Full month | `January` | | `%b` | Abbreviated month | `Jan` | | `%j` | Day of year | `015` | ## Min/max with dates ``` min([d("2024-01-15"), d("2024-03-20"), d("2024-02-10")]) // 2024-01-15 max([d("2024-01-15"), d("2024-03-20"), d("2024-02-10")]) // 2024-03-20 ``` ## Common patterns ### Age calculation ``` d().diff(birthDate, "year") ``` ### Days until expiration ``` d(expirationDate).diff(d(), "day") ``` ### Is within last 30 days ``` d(eventDate).isAfter(d().sub(30, "d")) ``` ### Business days check ``` d(orderDate).weekday() in [1..5] // 1=Monday, 5=Friday ``` # Built-in functions Source: https://docs.gorules.io/learn/zen-language/functions Complete reference for ZEN expression language built-in functions. ## Math functions ### abs Returns the absolute value. ``` abs(-5) // 5 abs(5) // 5 ``` ### floor Rounds down to the nearest integer. ``` floor(4.9) // 4 floor(-4.1) // -5 ``` ### ceil Rounds up to the nearest integer. ``` ceil(4.1) // 5 ceil(-4.9) // -4 ``` ### round Rounds to the nearest integer or decimal places. ``` round(4.5) // 5 round(4.4) // 4 round(4.567, 2) // 4.57 ``` ### trunc Truncates toward zero. ``` trunc(4.9) // 4 trunc(-4.9) // -4 ``` ### min Returns the minimum value from an array. ``` min([5, 2, 8, 1]) // 1 ``` ### max Returns the maximum value from an array. ``` max([5, 2, 8, 1]) // 8 ``` ### sum Returns the sum of array values. ``` sum([1, 2, 3, 4, 5]) // 15 ``` ### avg Returns the average of array values. ``` avg([10, 20, 30]) // 20 ``` ### median Returns the median of array values. ``` median([1, 2, 3, 4, 5]) // 3 ``` ### mode Returns the most frequent value. ``` mode([1, 2, 2, 3, 3, 3]) // 3 ``` ### rand Returns a random number between 0 and the specified maximum. ``` rand(100) // Random number 0-100 ``` ## String functions ### len Returns the length of a string or array. ``` len("hello") // 5 len([1, 2, 3]) // 3 ``` ### upper Converts to uppercase. ``` upper("hello") // "HELLO" ``` ### lower Converts to lowercase. ``` lower("HELLO") // "hello" ``` ### trim Removes leading and trailing whitespace. ``` trim(" hello ") // "hello" ``` ### contains Checks if a string contains a substring, or array contains a value. ``` contains("hello world", "world") // true contains([1, 2, 3], 2) // true ``` ### startsWith Checks if a string starts with a prefix. ``` startsWith("hello", "he") // true startsWith("hello", "lo") // false ``` ### endsWith Checks if a string ends with a suffix. ``` endsWith("hello", "lo") // true endsWith("hello", "he") // false ``` ### matches Tests a string against a regular expression. ``` matches("hello123", "[a-z]+[0-9]+") // true matches("123-456-7890", "[0-9]{3}-[0-9]{3}-[0-9]{4}") // true ``` ### extract Extracts groups from a regular expression match. ``` extract("2024-01-15", "(\d{4})-(\d{2})-(\d{2})") // ["2024-01-15", "2024", "01", "15"] ``` ### split Splits a string by delimiter. ``` split("a,b,c", ",") // ["a", "b", "c"] ``` ### fuzzyMatch Returns a similarity score (0-1) between strings. ``` fuzzyMatch("hello", "hello") // 1 fuzzyMatch("hello", "helo") // 0.8 ``` ## Array functions These functions iterate over arrays. Use `#` for the current element, or `as` for a named alias: ``` map(items, #.price) // using # map(items as item, item.price) // using alias filter(users as user, user.isActive) // more readable ``` ### map Transforms each element. ``` map([1, 2, 3], # * 2) // [2, 4, 6] map(items, #.price) // [prices...] map(users as u, { name: u.name, age: u.age }) ``` ### filter Keeps elements matching a condition. ``` filter([1, 2, 3, 4, 5], # > 3) // [4, 5] filter(items as item, item.price < 100) ``` ### some Returns true if any element matches. ``` some([1, 2, 3], # > 2) // true some(items, #.outOfStock) // true if any out of stock ``` ### all Returns true if all elements match. ``` all([1, 2, 3], # > 0) // true all(items, #.verified) // true if all verified ``` ### one Returns true if exactly one element matches. ``` one([1, 2, 3], # == 2) // true one([1, 2, 2], # == 2) // false ``` ### none Returns true if no elements match. ``` none([1, 2, 3], # > 10) // true ``` ### count Counts elements matching a condition. ``` count([1, 2, 2, 3, 3, 3], # == 3) // 3 ``` ### flatMap Maps and flattens results. ``` flatMap([[1, 2], [3, 4]], #) // [1, 2, 3, 4] ``` ### keys Returns object keys or array indices. ``` keys({ a: 1, b: 2 }) // ["a", "b"] keys([10, 20, 30]) // [0, 1, 2] ``` ### values Returns object values. ``` values({ a: 1, b: 2 }) // [1, 2] ``` ### merge Combines an array of arrays or an array of objects into a single result. Arrays - concatenates all arrays into one: ``` merge([[1, 2], [3, 4], [5]]) // [1, 2, 3, 4, 5] ``` Objects - combines all objects, last value wins for duplicate keys: ``` merge([{a: 1}, {b: 2}, {c: 3}]) // {a: 1, b: 2, c: 3} merge([{a: 1, b: 2}, {b: 3, c: 4}]) // {a: 1, b: 3, c: 4} ``` ### mergeDeep Recursively merges an array of objects. Unlike `merge`, nested objects are combined rather than replaced, and nested arrays are concatenated. Nested objects - recursively merged: ``` mergeDeep([{a: {x: 1}}, {a: {y: 2}}]) // {a: {x: 1, y: 2}} mergeDeep([{a: {b: {c: 1}}}, {a: {b: {d: 2}}}]) // {a: {b: {c: 1, d: 2}}} ``` Nested arrays - concatenated: ``` mergeDeep([{tags: [1, 2]}, {tags: [3, 4]}]) // {tags: [1, 2, 3, 4]} ``` Scalar values - last value wins: ``` mergeDeep([{a: 1}, {a: 2}]) // {a: 2} ``` ## Date functions ### d Creates a date object. See [Date operations](/learn/zen-language/dates) for full documentation. ``` d("2024-01-15") // Date object d("2024-01-15", "America/New_York") // With timezone d() // Current date/time ``` ### duration Parses a duration string to seconds. ``` duration("1h 30m") // 5400 duration("7d") // 604800 ``` ## Type functions ### string Converts to string. ``` string(123) // "123" string(true) // "true" ``` ### number Converts to number. ``` number("123") // 123 number("12.5") // 12.5 number(true) // 1 number(false) // 0 ``` ### bool Converts to boolean. ``` bool(1) // true bool(0) // false bool("true") // true ``` ### type Returns the type as a string. ``` type("hello") // "string" type(123) // "number" type(true) // "bool" type([1, 2]) // "array" type({a: 1}) // "object" type(null) // "null" ``` ### isNumeric Checks if a value is numeric or can be converted to a number. ``` isNumeric(123) // true isNumeric("123") // true isNumeric("hello") // false ``` # Operators Source: https://docs.gorules.io/learn/zen-language/operators Complete reference for ZEN expression operators. ZEN provides operators for arithmetic, comparison, logic, and data manipulation. ## Arithmetic operators Perform mathematical calculations on numbers. | Operator | Name | Example | Result | | -------- | ------------------ | -------- | ------ | | `+` | Addition | `5 + 3` | `8` | | `-` | Subtraction | `10 - 4` | `6` | | `*` | Multiplication | `6 * 7` | `42` | | `/` | Division | `15 / 3` | `5` | | `%` | Modulo (remainder) | `17 % 5` | `2` | | `^` | Power | `2 ^ 10` | `1024` | Division or modulo by zero returns `null` instead of failing the evaluation. Arithmetic that overflows the numeric range returns an error. ### Operator precedence Operations follow standard mathematical precedence: 1. `^` (power) 2. `*`, `/`, `%` (multiply, divide, modulo) 3. `+`, `-` (add, subtract) Use parentheses to control order: ``` (5 + 3) * 2 // 16, not 11 10 / (2 + 3) // 2, not 7 ``` ## Comparison operators Compare values and return boolean results. | Operator | Name | Example | Result | | -------- | ---------------- | -------- | ------ | | `==` | Equal | `5 == 5` | `true` | | `!=` | Not equal | `5 != 3` | `true` | | `>` | Greater than | `10 > 5` | `true` | | `<` | Less than | `3 < 10` | `true` | | `>=` | Greater or equal | `5 >= 5` | `true` | | `<=` | Less or equal | `3 <= 5` | `true` | ## Logical operators Combine boolean conditions. | Operator | Name | Example | Result | | -------- | ----------- | ---------------- | ------- | | `and` | Logical AND | `true and false` | `false` | | `or` | Logical OR | `true or false` | `true` | | `not` | Logical NOT | `not true` | `false` | ### Short-circuit evaluation Logical operators stop early when the result is determined: ``` false and expensiveFunction() // Never calls the function true or expensiveFunction() // Never calls the function ``` ## Ternary operator Conditionally return one of two values. ``` condition ? valueIfTrue : valueIfFalse ``` **Examples:** ``` age >= 18 ? "adult" : "minor" score >= 70 ? "pass" : "fail" stock > 0 ? "In Stock" : "Out of Stock" ``` **Nested ternary:** ``` score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F" ``` ## Null coalescing operator Return the first non-null value. ``` value ?? fallback ``` **Examples:** ``` user.nickname ?? user.name // Use nickname, fall back to name config.timeout ?? 30 // Use config or default to 30 a ?? b ?? c ?? "default" // Chain multiple fallbacks ``` ## Membership operators Check if a value exists in a collection or range. ### In operator ``` // Array membership "US" in ["US", "CA", "GB"] // true 5 in [1, 2, 3, 4, 5] // true // Range membership (inclusive) x in [1..10] // true if 1 <= x <= 10 // Range membership (exclusive) x in (0..100) // true if 0 < x < 100 // Mixed brackets x in [0..100) // true if 0 <= x < 100 x in (0..100] // true if 0 < x <= 100 ``` ### Not in operator Negate membership tests: ``` status not in ["deleted", "archived"] x not in [0..100] ``` ## String operators ### Concatenation The `+` operator joins strings: ``` "Hello" + " " + "World" // "Hello World" "Order #" + string(orderId) // "Order #12345" firstName + " " + lastName // "John Doe" ``` ### Template strings Embed expressions in strings: ``` `Hello, ${name}!` `Total: ${round(total, 2)}` `Items: ${len(items)}` ``` ## Array operators ### Index access ``` items[0] // First item items[len(items) - 1] // Last item items[-1] // Last item (negative index) ``` ## Object operators ### Property access ``` user.name // Dot notation user["name"] // Bracket notation user[fieldName] // Dynamic key ``` ### Optional chaining Access nested properties is optional: ``` user.address.city // Returns null if any part is null ``` ## Operator precedence table From highest to lowest precedence: | Precedence | Operators | | ---------- | ------------------------------------------------ | | 1 | `()` (grouping) | | 2 | `.`, `[]` (property access) | | 3 | `+`, `-` (unary) | | 4 | `??` (null coalescing) | | 5 | `^` (power, right-associative) | | 6 | `*`, `/`, `%` | | 7 | `not` (unary) | | 8 | `+`, `-` (binary) | | 9 | `==`, `!=`, `<`, `>`, `<=`, `>=`, `in`, `not in` | | 10 | `and` | | 11 | `or` | | 12 | `? :` (ternary) | # ZEN expression language Source: https://docs.gorules.io/learn/zen-language/syntax Complete syntax reference for the ZEN expression language. ZEN is GoRules' expression language for transforming data and evaluating conditions. It's designed to be readable by non-programmers while powerful enough for complex business logic. ## Two modes ZEN operates in two modes depending on context: | Mode | Used in | Example | | -------------- | -------------------------------- | ----------------------------------- | | **Standard** | Expression nodes, output columns | `price * quantity * (1 - discount)` | | **Unary test** | Decision table input columns | `>= 100`, `[1..10]`, `'US', 'CA'` | ## Standard mode Full expressions that return values. ### Literals ``` // Numbers 42 3.14 -17 1e6 // Strings "hello world" 'single quotes work too' `template with ${variables}` // Booleans true false // Null null // Arrays [1, 2, 3] ["a", "b", "c"] // Objects { name: "John", age: 30 } { [dynamicKey]: value } ``` ### Operators #### Arithmetic | Operator | Description | Example | | -------- | -------------- | ----------------- | | `+` | Addition | `5 + 3` → `8` | | `-` | Subtraction | `10 - 4` → `6` | | `*` | Multiplication | `6 * 7` → `42` | | `/` | Division | `15 / 3` → `5` | | `%` | Modulo | `17 % 5` → `2` | | `^` | Power | `2 ^ 10` → `1024` | #### Comparison | Operator | Description | Example | | -------- | ---------------- | --------- | | `==` | Equal | `x == 5` | | `!=` | Not equal | `x != 0` | | `>` | Greater than | `x > 10` | | `<` | Less than | `x < 100` | | `>=` | Greater or equal | `x >= 18` | | `<=` | Less or equal | `x <= 65` | #### Logical | Operator | Description | Example | | -------- | ----------- | --------- | | `and` | Logical AND | `a and b` | | `or` | Logical OR | `a or b` | | `not` | Logical NOT | `not a` | #### Ternary ``` condition ? valueIfTrue : valueIfFalse score >= 70 ? "pass" : "fail" age >= 18 ? "adult" : age >= 13 ? "teen" : "child" ``` #### Null coalescing Returns the first non-null value: ``` user.nickname ?? user.name ?? "Anonymous" ``` #### Range check ``` // Inclusive range x in [1..10] // true if 1 <= x <= 10 // Exclusive range x in (0..100) // true if 0 < x < 100 // Mixed x in [0..100) // true if 0 <= x < 100 x in (0..100] // true if 0 < x <= 100 // Negation x not in [1..10] ``` ### Property access ``` // Object properties customer.name customer.address.city // Array indexing items[0] items[0].price // Nested access order.items[0].product.name ``` ### Template strings ``` `Hello, ${name}!` `Total: ${sum(items)} items` `Status: ${approved ? 'Approved' : 'Pending'}` ``` ### String slicing Extract substrings using `[start:end]` notation: ``` string[0:5] // Characters 0-4 (first 5) string[7:12] // Characters 7-11 string[7:] // From index 7 to end string[:5] // First 5 characters (0-4) ``` | Expression | Input | Result | | ------------- | ----------------- | ---------- | | `string[0:5]` | `"sample_string"` | `"sampl"` | | `string[7:]` | `"sample_string"` | `"string"` | | `string[:6]` | `"sample_string"` | `"sample"` | ## Unary test mode Shorthand syntax used in decision table input columns when a **field name is defined**. The value being tested is implicitly available, allowing you to write conditions without repeating the field name. When an input column has no field name, standard expression mode is used instead. ### Comparisons ``` > 100 // Greater than 100 < 50 // Less than 50 >= 18 // Greater or equal to 18 <= 65 // Less or equal to 65 == "active" // Equal to "active" != 0 // Not equal to 0 ``` ### Ranges ``` [1..100] // Between 1 and 100 (inclusive) (0..100) // Between 0 and 100 (exclusive) [18..65) // 18 to 64 (0..100] // 1 to 100 ``` ### Lists ``` 'US', 'CA', 'GB' // Match any of these strings 1, 2, 3, 5, 8 // Match any of these numbers "pending", "processing" // Match any of these ``` ### Combined conditions ``` > 0 and < 100 // Between 0 and 100 >= 18 and <= 65 // Working age < 0 or > 100 // Outside 0-100 ``` ### Functions in unary mode ``` startsWith($, "PRE-") // String starts with prefix contains($, "urgent") // String contains substring len($) > 5 // Length greater than 5 ``` The `$` symbol represents the value being tested. ## Closures and iteration The `#` symbol represents the current element when iterating: ``` map([1, 2, 3], # * 2) // [2, 4, 6] map(items, #.price * #.quantity) // [totals...] filter([1, 2, 3, 4, 5], # > 3) // [4, 5] some([1, 2, 3], # > 2) // true all([1, 2, 3], # > 0) // true ``` ### Named aliases For readability, use `as` to name the current element: ``` map(cart.items as item, item.price * item.quantity) filter(users as user, user.isActive and user.age >= 18) some(orders as order, order.status == 'pending') ``` This is equivalent to using `#` but clearer when expressions are complex. ## Assignment Create values and build objects within expressions. ### Basic assignment ``` a = 5 // {"a": 5} name = 'John' // {"name": "John"} items = [1, 2, 3] // {"items": [1, 2, 3]} config = {debug: true} // {"config": {"debug": true}} ``` ### Property assignment Assign to nested paths - intermediate objects are created automatically: ``` user.name = 'Alice' // {"user": {"name": "Alice"}} user.profile.bio = 'Developer' // {"user": {"profile": {"bio": "Developer"}}} app.config.database.host = 'localhost' // Creates full nested structure ``` ### Multiple assignments Separate with semicolons: ``` a = 1; b = 2 // {"a": 1, "b": 2} user.name = 'Charlie'; user.age = 35 // {"user": {"name": "Charlie", "age": 35}} ``` ### Assignment with expressions ``` counter = counter + 1 // Increment existing value total = price * quantity // Compute from input fullName = firstName + ' ' + lastName // String concatenation doubled = map(numbers, # * 2) // Array operations status = score > 70 ? 'pass' : 'fail' // Conditional ``` ### Returning values The last expression determines the return value: ``` a = 5; b = 10; a + b // Returns 15 user.name = 'Eve'; user.name // Returns "Eve" config.debug = true; config // Returns {"debug": true} config.debug = true; $root // Returns {"config": {"debug": true}} ``` Use `$root` to return the entire context object.