Indicators Quickstart
Last Updated: 2026-06-08 API Version: v2.0 Base URL:
http://localhost:10001(development)
This guide walks through creating project-scoped indicators, applying them to controls, and sending events for evaluation.
Table of Contents
Section titled “Table of Contents”- Overview
- Prerequisites
- Core Concepts
- Creating Indicators
- Applying Indicators to Controls
- Sending Events
- Viewing Results
- Complete Example
- Troubleshooting
Overview
Section titled “Overview”The Indicators system provides a unified framework for compliance monitoring through custom indicators: event-driven evaluations that extract fields from incoming events using JSONPath and check them against criteria.
An indicator is a project-owned template that defines both what to monitor and how to evaluate it. Applying that indicator to one or more controls creates lightweight applications that link the template to specific controls. Events are then routed by projectId + indicatorId and fanned out to every active application of that indicator.
Architecture Flow
Section titled “Architecture Flow”┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐│ Create │ --> │ Apply to │ --> │ Send Events │ --> │ Results ││ Indicator │ │ Control(s) │ │ for Eval │ │ Declarations │└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ (Template w/ (Status-only (Ingestion, (Evidence) extraction + application per fans out to all criteria + control) active apps) declaration)Prerequisites
Section titled “Prerequisites”Required Setup
Section titled “Required Setup”-
Project & Control Context: You need:
- A project ID (and its project UUID for event ingestion)
- A policy attached to the project
- A control within the policy
- A
project_policy_controlID (the junction record linking a project policy to a control) for each control you apply an indicator to
-
Authentication:
- JWT Bearer token with project access and the appropriate permissions (creating, applying, activating, disabling, and archiving indicators are each gated by their own permission; reads require project-data access)
- Or an API key for service-to-service calls
-
Database:
- Governance service running locally or in your environment
- All database migrations applied (the latest relevant migration,
115_rename_configuration_to_application, finalizes the configuration → application rename)
Finding Your IDs
Section titled “Finding Your IDs”-- Find your project_policy_control_idSELECT ppc.id AS project_policy_control_id, p.name AS project_name, pp.name AS policy_name, c.title AS control_titleFROM project_policy_control ppcJOIN project_policy pp ON ppc.project_policy_id = pp.idJOIN project p ON pp.project_id = p.idJOIN control c ON ppc.control_id = c.idWHERE p.id = <YOUR_PROJECT_ID> AND pp.policy_id = <YOUR_POLICY_ID> AND c.id = <YOUR_CONTROL_ID>;Project-scoped indicator and application routes use the numeric project ID in the path (
/api/v2/projects/{projectId}/...). Event ingestion uses the project UUID in theprojectIdbody field.
Core Concepts
Section titled “Core Concepts”1. Indicators (Project-Scoped Templates)
Section titled “1. Indicators (Project-Scoped Templates)”Indicators are immutable templates owned by a project. A single indicator carries everything needed to evaluate an event:
- Identity:
name,slug,description,type(custom),tags,provider triggerType(event)extractionRules: JSONPath expressions that pull fields out of an eventcriteria: the conditions that determine pass/fail (all must pass)declarationProfile: when and how to create declarations
Key Point: In v2 the evaluation behavior lives on the indicator template, not on a separate application object.
2. Applications (Status-Only Links to Controls)
Section titled “2. Applications (Status-Only Links to Controls)”An application links an indicator to a single project_policy_control. It is
status-only — it carries an active / disabled / archived status and inherits all
extraction, criteria, and declaration behavior from its indicator template at evaluation
time.
Key Point: One indicator can be applied to many controls. Each application is an independent on/off switch for that indicator on a specific control.
3. Events & Evaluations
Section titled “3. Events & Evaluations”When you send an indicator event:
- The service resolves the project UUID and looks up the indicator
- It fans out to all active applications of that indicator in the project
- For each application, the processor extracts fields and evaluates criteria
- Results are stored as evaluation results
- Declarations are created based on the indicator’s declaration profile
- Evidence is preserved for the audit trail
Creating Indicators
Section titled “Creating Indicators”Endpoint
Section titled “Endpoint”POST /api/v2/projects/{projectId}/indicatorsContent-Type: application/jsonAuthorization: Bearer <token>Custom Indicator Example
Section titled “Custom Indicator Example”Use Case: Monitor S3 bucket encryption
{ "type": "custom", "name": "S3 Bucket Encryption Status", "description": "Monitors whether S3 buckets have encryption enabled", "tags": ["aws", "s3", "encryption", "storage"], "provider": "aws", "triggerType": "event", "extractionRules": { "bucketName": "$.detail.bucket.name", "encryptionEnabled": "$.detail.bucket.encryption.enabled", "encryptionType": "$.detail.bucket.encryption.type", "region": "$.detail.bucket.region" }, "criteria": [ { "type": "boolean", "field": "encryptionEnabled", "operator": "eq", "criteriaValue": true }, { "type": "string", "field": "encryptionType", "operator": "in", "criteriaValue": ["AES256", "aws:kms"] } ], "declarationProfile": { "emitOn": "fail", "titleTemplate": "S3 Bucket {{bucketName}} Missing Encryption", "statementTemplate": "The S3 bucket {{bucketName}} in region {{region}} does not have encryption enabled. All buckets must use AES256 or KMS encryption." }}Field Breakdown:
| Field | Required | Description |
|---|---|---|
type | Yes | Must be "custom" (defaults to custom if omitted) |
name | Yes | Human-readable indicator name |
slug | No | Stable identifier; generated from name if omitted |
description | No | Free-text description |
tags | No | Tags for discovery/filtering |
provider | No | Provider hint (e.g., "aws") |
triggerType | Yes | Must be "event" (defaults to event if omitted) |
extractionRules | Yes | JSONPath expressions to extract fields from the event |
criteria | Yes | Conditions to evaluate (all must pass) |
declarationProfile | Yes | When and how to create declarations (emitOn is required) |
Response (201 Created):
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "projectId": 456, "name": "S3 Bucket Encryption Status", "slug": "s3-bucket-encryption-status", "description": "Monitors whether S3 buckets have encryption enabled", "type": "custom", "tags": ["aws", "s3", "encryption", "storage"], "provider": "aws", "triggerType": "event", "extractionRules": { /* as provided */ }, "criteria": [ /* as provided */ ], "declarationProfile": { /* as provided */ }, "createdAt": "2026-06-08T10:00:00Z", "updatedAt": "2026-06-08T10:00:00Z", "usageCount": 0}List Indicators
Section titled “List Indicators”GET /api/v2/projects/{projectId}/indicators?search=encryption&tag=aws&limit=50&offset=0Supported query parameters: search (matches name/description), tag, limit
(1–500, default 50), offset.
Response:
{ "indicators": [ { /* project indicator object, including usageCount */ } ], "total": 15, "limit": 50, "offset": 0}Get / Archive an Indicator
Section titled “Get / Archive an Indicator”# Get one indicator (includes usageCount)GET /api/v2/projects/{projectId}/indicators/{indicatorId}
# Archive an indicator (only allowed when it has no non-archived applications)DELETE /api/v2/projects/{projectId}/indicators/{indicatorId}Archiving returns 204 No Content. If the indicator still has active applications, the
request returns 409 Conflict.
Applying Indicators to Controls
Section titled “Applying Indicators to Controls”Applying an indicator creates one status-only application per target control.
Endpoint
Section titled “Endpoint”POST /api/v2/projects/{projectId}/indicators/{indicatorId}/applicationsContent-Type: application/jsonAuthorization: Bearer <token>Request
Section titled “Request”{ "projectPolicyControlIds": [12345, 12346], "status": "active"}| Field | Required | Description |
|---|---|---|
projectPolicyControlId | One of* | Single project_policy_control ID |
projectPolicyControlIds | One of* | List of project_policy_control IDs to apply in one call |
status | No | active (default) or disabled |
*Provide projectPolicyControlId, projectPolicyControlIds, or both; all referenced
controls must belong to the project.
Response (201 Created):
{ "applications": [ { "id": 789, "applicationUuid": "f6a7b8c9-d0e1-2345-f012-456789012345", "indicatorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "projectPolicyControlId": 12345, "status": "active", "createdAt": "2026-06-08T10:15:00Z", "updatedAt": "2026-06-08T10:15:00Z" } ]}Listing & Managing Applications
Section titled “Listing & Managing Applications”# List applications for a single indicatorGET /api/v2/projects/{projectId}/indicators/{indicatorId}/applications
# List enriched applications across the project (template + policy/control context +# latest evaluation). Filters: indicatorId, projectPolicyId, projectPolicyControlId,# applicationUuid, status, includeArchived, limit, offset.GET /api/v2/projects/{projectId}/indicator-applications
# Get one enriched application by numeric ID or application UUIDGET /api/v2/projects/{projectId}/indicator-applications/{applicationId}
# Activate an applicationPOST /api/v2/projects/{projectId}/indicator-applications/{applicationId}/enable
# Disable an application (stops it from being evaluated)POST /api/v2/projects/{projectId}/indicator-applications/{applicationId}/disable
# Remove (archive) an applicationDELETE /api/v2/projects/{projectId}/indicator-applications/{applicationId}enable and disable return the updated application; delete archives the application
and returns 204 No Content.
Sending Events
Section titled “Sending Events”Event Ingestion Endpoint
Section titled “Event Ingestion Endpoint”POST /api/v2/events/ingestContent-Type: application/jsonAuthorization: Bearer <token>The endpoint accepts the event and returns 202 Accepted; evaluation happens against every
active application of the indicator.
Indicator Event
Section titled “Indicator Event”Example: S3 bucket state change event
{ "eventType": "indicator", "eventSubtype": "resource_state", "source": "aws", "timestamp": "2026-06-08T10:30:00Z", "projectId": "9f8e7d6c-5b4a-3210-fedc-ba9876543210", "indicatorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "provider": "aws", "service": "s3", "payload": { "version": "0", "id": "12345678-1234-1234-1234-123456789012", "detail-type": "AWS API Call via CloudTrail", "source": "aws.s3", "account": "123456789012", "time": "2026-06-08T10:30:00Z", "region": "us-east-1", "detail": { "bucket": { "name": "my-production-bucket", "encryption": { "enabled": false }, "region": "us-east-1" } } }}Request Fields:
| Field | Required | Description |
|---|---|---|
eventType | Yes | Must be "indicator" |
eventSubtype | No | Event category (e.g., "resource_state") |
source | Yes | Event source (e.g., "aws", "external") |
timestamp | Yes | ISO 8601 timestamp |
projectId | Yes | Project UUID (resolved server-side, access checked) |
indicatorId | Yes | Indicator UUID to route the event to |
provider | No | Provider hint (e.g., "aws") |
service | No | Service hint (e.g., "s3") |
payload | Yes | Event data (any JSON structure) |
correlationId | No | For event tracing |
metadata | No | Additional metadata |
Removed:
controlIdis no longer accepted for routing —projectId+indicatorIdfans out to all active applications of the indicator. SendingcontrolIdreturns400 Bad Request. There is also nopolicyevent type orindicatorNamerouting key in v2.
Routing & Processing:
- The project UUID is resolved to a project and access is verified
- The indicator is looked up and confirmed to be an active, project-scoped indicator
- The event fans out to all active applications of that indicator
- For each application, the processor extracts fields using
extractionRules, evaluatescriteria, stores an evaluation result, and creates a declaration when thedeclarationProfiledictates
Event Response
Section titled “Event Response”{ "eventId": "12345678-1234-1234-1234-123456789012", "status": "processed", "evaluationsCreated": 1, "declarationsCreated": [567]}Status Values:
processed: Event matched one or more active applications and was evaluatedno_route: No active application matched (unknown/archived indicator, or none applied)
If individual applications fail to process, the response includes an errors array while
still reporting the evaluations and declarations that succeeded.
Viewing Results
Section titled “Viewing Results”Evaluation Results
Section titled “Evaluation Results”# List evaluation results (filters below)GET /api/v2/evaluations?applicationId=789&result=fail&limit=50&offset=0
# Get a single evaluation result by UUIDGET /api/v2/evaluations/{id}Supported list filters: applicationId, indicatorId, projectId, projectPolicyId,
projectPolicyControlId, result (pass, fail, error, pending),
since/until (ISO 8601), limit (1–200, default 50), offset.
Response:
{ "results": [ { "id": "eval1234-1234-1234-1234-123456789012", "applicationId": 789, "indicatorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "result": "fail", "extractedFields": { "bucketName": "my-production-bucket", "encryptionEnabled": false, "region": "us-east-1" }, "failureReasons": ["Criteria failed: encryptionEnabled must be true"], "payloadTruncated": false, "evaluatedAt": "2026-06-08T10:30:05Z", "durationMs": 45, "declarationIds": [567], "createdAt": "2026-06-08T10:30:05Z" } ], "total": 1, "limit": 50, "offset": 0}Declarations
Section titled “Declarations”Declarations are created in the existing declaration table:
# Get declarations for a controlGET /api/v1/projects/{projectId}/policies/{projectPolicyId}/controls/{projectPolicyControlId}/declarationsDeclaration Fields:
title: Generated fromtitleTemplatewith extracted fieldsstatement: Generated fromstatementTemplatewith extracted fieldsevidence: Link to the evaluation result and raw event payload
Complete Example
Section titled “Complete Example”S3 Encryption Monitoring (End-to-End)
Section titled “S3 Encryption Monitoring (End-to-End)”Step 1: Create the Indicator
Section titled “Step 1: Create the Indicator”curl -X POST http://localhost:10001/api/v2/projects/456/indicators \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "type": "custom", "name": "S3 Bucket Encryption", "description": "Ensures S3 buckets have encryption enabled", "tags": ["aws", "s3", "encryption"], "provider": "aws", "triggerType": "event", "extractionRules": { "bucketName": "$.detail.bucket.name", "encryptionEnabled": "$.detail.bucket.encryption.enabled" }, "criteria": [ { "type": "boolean", "field": "encryptionEnabled", "operator": "eq", "criteriaValue": true } ], "declarationProfile": { "emitOn": "fail", "titleTemplate": "Bucket {{bucketName}} lacks encryption", "statementTemplate": "S3 bucket {{bucketName}} must have encryption enabled" } }'Save the id from the response (e.g., 550e8400-e29b-41d4-a716-446655440000).
Step 2: Apply to Your Control(s)
Section titled “Step 2: Apply to Your Control(s)”curl -X POST http://localhost:10001/api/v2/projects/456/indicators/550e8400-e29b-41d4-a716-446655440000/applications \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "projectPolicyControlIds": [12345], "status": "active" }'Step 3: Send an Event
Section titled “Step 3: Send an Event”curl -X POST http://localhost:10001/api/v2/events/ingest \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "eventType": "indicator", "eventSubtype": "resource_state", "source": "aws", "timestamp": "2026-06-08T10:30:00Z", "projectId": "9f8e7d6c-5b4a-3210-fedc-ba9876543210", "indicatorId": "550e8400-e29b-41d4-a716-446655440000", "provider": "aws", "service": "s3", "payload": { "detail": { "bucket": { "name": "my-test-bucket", "encryption": { "enabled": false } } } } }'Step 4: View Results
Section titled “Step 4: View Results”# Evaluation results for the applicationcurl "http://localhost:10001/api/v2/evaluations?indicatorId=550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $TOKEN"
# Declarations on the controlcurl "http://localhost:10001/api/v1/projects/456/policies/$PROJECT_POLICY_ID/controls/$PROJECT_POLICY_CONTROL_ID/declarations" \ -H "Authorization: Bearer $TOKEN"Troubleshooting
Section titled “Troubleshooting”Event Not Processed
Section titled “Event Not Processed”Symptom: Event returns no_route status
Causes:
- The indicator UUID does not exist, is archived, or is not a project-scoped indicator
- No active application exists for the indicator (it was never applied, or all applications are disabled/archived)
- Wrong
eventType(must be"indicator")
Solutions:
# 1. Confirm the indicator exists and is activeGET /api/v2/projects/{projectId}/indicators/{indicatorId}
# 2. Confirm active applications exist for the indicatorGET /api/v2/projects/{projectId}/indicators/{indicatorId}/applications
# 3. Enable a disabled applicationPOST /api/v2/projects/{projectId}/indicator-applications/{applicationId}/enableWrong Project or Access Errors
Section titled “Wrong Project or Access Errors”Symptom: Event returns 400 Invalid projectId or 403 Forbidden
Causes:
projectIdin the event body is not a valid project UUID- The authenticated user/token does not have access to the resolved project
Solution: Use the project UUID (not the numeric ID) in the event projectId field, and
ensure the token has project access.
controlId Rejected
Section titled “controlId Rejected”Symptom: Event returns 400 with “controlId routing is no longer supported”
Cause: v2 routes by projectId + indicatorId and fans out to all active applications.
Remove controlId from the event body. To scope evaluation to specific controls, control
which applications are active.
Evaluation Always Fails
Section titled “Evaluation Always Fails”Symptom: All evaluations show result: "fail"
Causes:
- JSONPath extraction rules don’t match the event structure
- Criteria types don’t match extracted field types
- Criteria operators incorrect for the data type
Solutions:
# Test JSONPath expressions online: https://jsonpath.com# Inspect extractedFields and failureReasons on the evaluation resultGET /api/v2/evaluations?indicatorId=<UUID>No Declaration Created
Section titled “No Declaration Created”Symptom: Evaluation succeeds but no declaration appears
Cause: The indicator’s declarationProfile.emitOn did not match the result:
emitOn: "fail"only creates on failuresemitOn: "pass"only creates on passesemitOn: "both"creates on eitheremitOn: "none"never creates
Solution: Check the indicator template’s declaration profile and confirm the evaluation result matches the configured emit mode.
Finding project_policy_control_id
Section titled “Finding project_policy_control_id”Symptom: Don’t know what value to use for projectPolicyControlId
Solution:
SELECT ppc.id, p.name, pp.name, c.titleFROM project_policy_control ppcJOIN project_policy pp ON ppc.project_policy_id = pp.idJOIN project p ON pp.project_id = p.idJOIN control c ON ppc.control_id = c.idWHERE p.id = <YOUR_PROJECT_ID> AND c.id = <YOUR_CONTROL_ID>;Or use the API:
GET /api/v1/projects/{projectId}/policies/{projectPolicyId}/details# Find the control you want and use its projectPolicyControlIdInvalid JSONPath Expression
Section titled “Invalid JSONPath Expression”Symptom: Error “invalid JSONPath expression”
Common Issues:
- Missing
$prefix: Use$.detail.bucket.namenotdetail.bucket.name - Array access: Use
$.items[0].nameor$.items[*].name - Deep nesting: Each level needs a dot:
$.a.b.c.d
Tool: Test at https://jsonpath.com
Criteria Not Matching
Section titled “Criteria Not Matching”Symptom: Evaluation fails with “Criteria failed” but values look correct
Type Mismatches:
// Wrong: string criteria for a boolean field{ "type": "string", "field": "encryptionEnabled", "operator": "eq", "criteriaValue": "true" // ❌ String}
// Correct: boolean criteria{ "type": "boolean", "field": "encryptionEnabled", "operator": "eq", "criteriaValue": true // ✅ Boolean}Operator Compatibility:
| Type | Valid Operators |
|---|---|
string | eq, neq, in, notIn, contains, notContains |
number | eq, neq, gt, lt, gte, lte, in, notIn |
boolean | eq, neq |
date | eq, neq, gt, lt, gte, lte |
list[string] | in, notIn |
list[number] | in, notIn |
Additional Resources
Section titled “Additional Resources”- OpenAPI Spec:
governance-service/docs/swagger.yaml(authoritative request/response shapes) - Go Types:
governance-service/pkg/core/indicators.go— indicator template and declaration profilegovernance-service/pkg/core/applications.go— indicator applicationgovernance-service/pkg/core/events.go— event ingestion request/responsegovernance-service/pkg/core/evaluation.go— criteria, operators, evaluation results
- API Handlers:
governance-service/pkg/handlers/indicators.go— project indicator + application managementgovernance-service/pkg/handlers/applications.go— evaluation result endpointsgovernance-service/pkg/handlers/events_ingestion.go— event ingestion endpoint
- Routing:
governance-service/pkg/routes/routes.go— v2 route registration
Summary
Section titled “Summary”The Indicators system follows a three-step process:
- Create Indicator → A project-owned template that defines what to monitor and how to evaluate it (extraction rules, criteria, declaration profile)
- Apply to Control(s) → Status-only applications that switch the indicator on for specific controls
- Send Events → Routed by
projectId+indicatorId, fanned out to all active applications for evaluation and evidence
This separation allows:
- ✅ Reusing one indicator across multiple controls
- ✅ Per-control on/off control via application status
- ✅ Centralized event ingestion with fan-out routing
- ✅ Automatic declaration generation with templated messaging
Key Takeaway: The indicator is the template (identity + extraction + criteria + declaration); applications are the status-only links that decide which controls it runs on.