Recommendations
ZopNight analyzes your cloud resources and generates cost optimization recommendations based on usage patterns, idle detection, and best practices.
How Recommendations Work
The recommender service subscribes to resource discovery events via Redis Streams. When new or updated resources are discovered, it runs audit rules across all three cloud providers to generate actionable recommendations.
Recommendation Categories
| Category | Description |
|---|---|
idle | Resources with low or no utilization that could be stopped or terminated |
rightsizing | Resources that are over-provisioned and could use a smaller instance type |
schedule | Resources that would benefit from a start/stop schedule or autoscaler policy |
orphan | Unattached resources (volumes, snapshots, IPs) with ongoing costs |
compliance | Resources missing required tags or not following naming conventions |
discount | Commitment-based savings opportunities (Reserved Instances, Savings Plans) |
governance | Best practice violations (e.g., public access, missing encryption) |
Severity Levels
| Severity | Description |
|---|---|
| critical | High-cost waste requiring immediate attention |
| high | Significant savings opportunity |
| medium | Moderate optimization potential |
| low | Minor improvement suggestion |
| info | Informational — no direct cost impact |
List Resource Summaries
/recommendations/resourcesList distinct resources with aggregate recommendation stats. Used for the resource view.
Query Parameters
| Parameter | Description |
|---|---|
status | Filter by recommendation status (open, applied, dismissed, optimised) |
severity | Filter by severity level |
category | Filter by category |
provider | Filter by cloud provider |
resource_type | Filter by resource type (ec2, rds, disk, etc.) |
cloud_account_id | Filter by cloud account ID (comma-separated for multiple) |
search | Search by resource name or UID |
sort_by | Sort column (savings_usd, updated_at, severity). Default: savings_usd |
sort_order | Sort direction (asc, desc). Default: desc |
page | Page number (default: 1) |
size | Items per page (default: 10, max: 100) |
{
"data": {
"items": [
{
"resourceUid": "i-0abc123def456",
"resourceName": "idle-dev-server",
"resourceType": "ec2",
"provider": "aws",
"cloudAccountId": "123456789012",
"cloudAccountName": "AWS Prod",
"recommendationCount": 3,
"openCount": 2,
"totalSavingsUsd": 52.56
}
],
"total": 45,
"page": 1,
"limit": 10,
"hasMore": true
}
}List Rule Summaries
/recommendations/rulesList distinct rules with aggregate recommendation stats. Used for the rule view.
Query Parameters
Same filters as /recommendations/resources plus:
| Parameter | Description |
|---|---|
rule_id | Filter by specific rule ID (e.g., RC-212) |
{
"data": {
"items": [
{
"ruleId": "RC-212",
"title": "Azure managed disk is unattached — verify and delete if not needed",
"category": "orphan",
"severity": "low",
"resourceCount": 36,
"openCount": 35,
"totalSavingsUsd": 5.00
}
],
"total": 8,
"page": 1,
"limit": 10,
"hasMore": false
}
}List Recommendations
/recommendationsList individual recommendations with filtering and pagination. Use resource_uid or rule_id to scope to a specific group.
Query Parameters
Same filters as /recommendations/resources plus:
| Parameter | Description |
|---|---|
resource_uid | Filter by resource UID (used when expanding a resource card) |
rule_id | Filter by rule ID (used when expanding a rule card) |
{
"data": {
"items": [
{
"id": "rec_001",
"resourceUid": "i-0abc123def456",
"resourceName": "idle-dev-server",
"resourceType": "ec2",
"ruleId": "RC-001",
"title": "Idle EC2 Instance",
"description": "This instance has had less than 5% CPU utilization over the past 14 days.",
"currentCostUsd": 52.56,
"optimizedCostUsd": 0.00,
"savingsUsd": 52.56,
"status": "open",
"severity": "high",
"category": "idle",
"remediation": "Consider stopping or terminating this instance if it is not needed.",
"consoleUrl": "https://console.aws.amazon.com/ec2/home?region=us-east-1#Instances:instanceId=i-0abc123def456",
"provider": "aws",
"cloudAccountId": "123456789012",
"cloudAccountName": "AWS Prod",
"actionType": "stop",
"evidence": {
"metric": "CPUUtilization",
"windowDays": 14,
"p95": 3.1
},
"generatedAt": "2025-01-20T08:00:00Z"
}
],
"total": 36,
"page": 1,
"limit": 5,
"hasMore": true
}
}Recommendation Summary
/recommendations/summaryGet aggregate recommendation statistics.
{
"data": {
"totalOpen": 23,
"totalSavings": 1250.80,
"criticalCount": 2,
"highCount": 8,
"resourceCount": 18,
"awsResources": 10,
"gcpResources": 5,
"azureResources": 3,
"appliedCount": 12,
"dismissedCount": 5,
"optimisedCount": 3
}
}Resource Recommendations (Deprecated)
/recommendations/resources/{resourceUID}Get all recommendations for a specific resource.
Deprecated
GET /recommendations?resource_uid={resourceUID} instead, which supports pagination. Target removal: 2026-10-01.Update Recommendation
/recommendations/{recommendationID}Mark a recommendation as applied or dismissed, or reopen one that was previously applied or dismissed.
curl -X PATCH https://zopnight.com/api/recommendations/rec_001 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "status": "applied" }'Valid status transitions:
open→appliedopen→dismissedapplied→open(reopen)dismissed→open(reopen)
Savings on Applied & Auto-Resolved
totalSavingsUSD field on the Applied and Auto-Resolved recommendation cards reflects realised savings, not just open opportunity. The /recommendations/summary endpoint exposes the same numbers via appliedSavingsUSD and optimisedSavingsUSD.Filter Fields
/recommendations/filter-fieldsGet available filter field values for building filter UIs.
{
"data": {
"statuses": ["open", "applied", "dismissed", "optimised"],
"severities": ["critical", "high", "medium", "low", "info"],
"categories": ["idle", "rightsizing", "schedule", "orphan", "compliance", "discount", "governance"],
"providers": ["aws", "gcp", "azure"]
}
}Provider Breakdown
/recommendations/summary/providersGet recommendation counts and savings broken down by cloud provider.
{
"data": [
{ "provider": "aws", "totalOpen": 12, "totalSavings": 820.50, "criticalCount": 1, "highCount": 5 },
{ "provider": "gcp", "totalOpen": 6, "totalSavings": 310.00, "criticalCount": 0, "highCount": 2 },
{ "provider": "azure", "totalOpen": 5, "totalSavings": 120.30, "criticalCount": 1, "highCount": 1 }
]
}Recommendation Detail
/recommendations/{recID}/detailGet full detail for a single recommendation including metrics and remediation steps.
Refresh Recommendations
/recommendations/refreshTrigger a recommendation recomputation for the organization.
Resource Intensive
Auto-Remediation Workflows
For auto-remediable rules (rightsizing, resize, oversized) a recommendation can be applied through a multi-step workflow that the provisioner executes against the cloud provider. Workflows are previewed, started, and approved through the endpoints below. See Provisioning for the apply-side detail.
/recommendations/{recID}/workflowPreview the workflow plan that would be created for this recommendation — steps, target spec, and approval gates — without persisting anything.
/recommendations/{recID}/workflowStart a remediation workflow for this recommendation. Returns a workflow job ID; poll /workflows/{jobID} for status.
/workflows/pending-approvalList workflow steps across the org that are waiting on a human approver. Used by the approvals queue widget.
/workflows/{jobID}Get a workflow's status, current step, and per-step audit trail.
/workflows/{jobID}/cancelCancel an in-flight workflow. Steps already applied are not rolled back; subsequent steps are skipped.
/workflows/{jobID}/steps/{stepName}/approveApprove a paused workflow step by its declared name. Use the by-id variant when you have the step's stable ID instead of the template name.
/workflows/{jobID}/steps/{stepName}/rejectReject a paused workflow step. The workflow halts and no further steps are dispatched.
/workflows/{jobID}/steps-by-id/{stepID}/approveApprove a paused workflow step by its stable ID. Prefer this when the UI already has the step ID — it survives template renames.
/workflows/{jobID}/steps-by-id/{stepID}/rejectReject a paused workflow step by its stable ID.
Notification Policies
Notification policies declaratively route matching recommendation findings to notification channels. A policy attaches a selector (which findings to match) and a notify action (which channel IDs to send to) to a scope — organisation or cloud_account. When a new finding surfaces, the most specific matching policy wins and its channels are notified once per surfaced episode.
Policies are managed under Settings → Policy and require policy:view / policy:create / policy:update / policy:delete permissions (see Roles & Permissions). Notification channels (the IDs used in notify.channels) are managed under Notifications.
Scope precedence
When a finding matches policies at multiple scopes, the most specific scope wins. Tie-breaks: earliest created_at, then lower id.
| Scope | target_type | Priority (higher = more specific) |
|---|---|---|
| Organisation | organisation | 0 (lowest) |
| Cloud account | cloud_account | 1 |
Policy spec
The policy spec is a JSON object owned by the recommendation domain. It has two keys: selector (which findings to match) and notify (what to do when the selector matches).
{
"selector": {
"all": [
{ "attr": "category", "op": "in", "values": ["idle", "rightsizing"] },
{ "attr": "severity", "op": "in", "values": ["critical", "high"] },
{ "attr": "resourceType", "op": "notIn", "values": ["rds"] }
]
},
"notify": {
"channels": ["ch_abc123", "ch_def456"]
}
}Selector combinators
| Key | Behaviour |
|---|---|
all | All child clauses must match (AND). Empty array → matches everything. |
any | At least one child clause must match (OR). Empty array → matches nothing. |
not | Inverts a single child clause. |
(absent / {}) | No selector → matches every finding (catch-all policy). |
Selector leaf attributes
| attr | Valid values |
|---|---|
category | idle, rightsizing, schedule, orphan, compliance, discount, governance |
severity | critical, high, medium, low, info |
resourceType | Any resource type string (ec2, rds, disk, …) — use the catalog endpoint to get the full list |
Fail-open on empty, fail-closed on malformed
selector matches all findings. A malformed selector (unknown attribute, unsupported operator, empty in/notIn list) matches nothing, so a broken policy is never a catch-all.Policy catalog
/recommendations/policies/catalogReturns the policy form vocabulary: available categories, match facets (category / severity / resourceType), offered actions, and creatable scope types. Use this to build or validate a policy form.
{
"data": {
"scopes": ["organisation", "cloud_account"],
"actions": ["notify"],
"facets": [
{ "attr": "category", "label": "Category", "op": ["in", "notIn"], "values": ["idle", "rightsizing", "schedule", "orphan", "compliance", "discount", "governance"] },
{ "attr": "severity", "label": "Severity", "op": ["in", "notIn"], "values": ["critical", "high", "medium", "low", "info"] },
{ "attr": "resourceType", "label": "Resource type", "op": ["in", "notIn"], "values": ["ec2", "rds", "disk", "gce", "azure-vm", "..."] }
]
}
}Validate a policy spec
Validate before create
valid: true./recommendations/policies/validationsStateless pre-create validation. Returns { valid, errors } and persists nothing.
curl -X POST https://zopnight.com/api/recommendations/policies/validations \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"selector": { "all": [{ "attr": "severity", "op": "in", "values": ["critical"] }] },
"notify": { "channels": ["ch_abc123"] }
}
}'{ "data": { "valid": true, "errors": [] } }{
"data": {
"valid": false,
"errors": [
{ "field": "spec.notify.channels", "message": "at least one channel is required" }
]
}
}List policies
/policiesList the organisation's recommendation policies (paginated). Pass domain=recommendation to scope to this feature.
{
"data": {
"items": [
{
"id": "pol_abc123",
"domain": "recommendation",
"targetType": "organisation",
"targetId": "org_xyz",
"name": "Critical findings → Slack",
"description": "Route critical and high severity findings to #alerts",
"spec": {
"selector": { "all": [{ "attr": "severity", "op": "in", "values": ["critical", "high"] }] },
"notify": { "channels": ["ch_abc123"] }
},
"enabled": true,
"createdBy": "admin@company.com",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20,
"hasMore": false
}
}Create a policy
/policiesCreate a notification policy. Validate the spec with the validations endpoint first.
curl -X POST https://zopnight.com/api/policies \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"domain": "recommendation",
"targetType": "organisation",
"targetId": "org_xyz",
"name": "Critical findings → Slack",
"description": "Route critical and high severity findings to #alerts",
"spec": {
"selector": {
"all": [
{ "attr": "severity", "op": "in", "values": ["critical", "high"] }
]
},
"notify": {
"channels": ["ch_abc123"]
}
}
}'{
"data": {
"id": "pol_abc123",
"domain": "recommendation",
"targetType": "organisation",
"targetId": "org_xyz",
"name": "Critical findings → Slack",
"enabled": true,
"createdAt": "2025-01-15T10:30:00Z"
}
}/policies/{id}Get a single policy including its full spec.
/policies/{id}Update a policy's name, description, spec, or enabled flag.
curl -X PATCH https://zopnight.com/api/policies/pol_abc123 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'/policies/{id}Soft-delete a policy. Deleted policies stop matching immediately.
Smart Tags
ZopNight derives virtual tags from tagging policies you define and stores them per resource (pending until you accept). They power tag-based cost attribution and are never written back to the cloud. The full API — GET/PATCH /smart-tags plus the tagging-policy catalog and validation endpoints — lives on its own page: Smart Tags.
The recommender runs 337+ audit rules across AWS (155), GCP (75), and Azure (107), including six autoscaler rules (RC-ASC-001..006) that feed into VM Autoscaling. See Cloud Support Matrix for provider-specific details.