Documentation

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

CategoryDescription
idleResources with low or no utilization that could be stopped or terminated
rightsizingResources that are over-provisioned and could use a smaller instance type
scheduleResources that would benefit from a start/stop schedule or autoscaler policy
orphanUnattached resources (volumes, snapshots, IPs) with ongoing costs
complianceResources missing required tags or not following naming conventions
discountCommitment-based savings opportunities (Reserved Instances, Savings Plans)
governanceBest practice violations (e.g., public access, missing encryption)

Severity Levels

SeverityDescription
criticalHigh-cost waste requiring immediate attention
highSignificant savings opportunity
mediumModerate optimization potential
lowMinor improvement suggestion
infoInformational — no direct cost impact

List Resource Summaries

GET
/recommendations/resources

List distinct resources with aggregate recommendation stats. Used for the resource view.

Query Parameters

ParameterDescription
statusFilter by recommendation status (open, applied, dismissed, optimised)
severityFilter by severity level
categoryFilter by category
providerFilter by cloud provider
resource_typeFilter by resource type (ec2, rds, disk, etc.)
cloud_account_idFilter by cloud account ID (comma-separated for multiple)
searchSearch by resource name or UID
sort_bySort column (savings_usd, updated_at, severity). Default: savings_usd
sort_orderSort direction (asc, desc). Default: desc
pagePage number (default: 1)
sizeItems per page (default: 10, max: 100)
Responsejson
{
  "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

GET
/recommendations/rules

List distinct rules with aggregate recommendation stats. Used for the rule view.

Query Parameters

Same filters as /recommendations/resources plus:

ParameterDescription
rule_idFilter by specific rule ID (e.g., RC-212)
Responsejson
{
  "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

GET
/recommendations

List 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:

ParameterDescription
resource_uidFilter by resource UID (used when expanding a resource card)
rule_idFilter by rule ID (used when expanding a rule card)
Responsejson
{
  "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

GET
/recommendations/summary

Get aggregate recommendation statistics.

Responsejson
{
  "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)

GET
/recommendations/resources/{resourceUID}

Get all recommendations for a specific resource.

Deprecated

Use GET /recommendations?resource_uid={resourceUID} instead, which supports pagination. Target removal: 2026-10-01.

Update Recommendation

PATCH
/recommendations/{recommendationID}

Mark a recommendation as applied or dismissed, or reopen one that was previously applied or dismissed.

Requestbash
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:

  • openapplied
  • opendismissed
  • appliedopen (reopen)
  • dismissedopen (reopen)

Savings on Applied & Auto-Resolved

The 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

GET
/recommendations/filter-fields

Get available filter field values for building filter UIs.

Responsejson
{
  "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

GET
/recommendations/summary/providers

Get recommendation counts and savings broken down by cloud provider.

Responsejson
{
  "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

GET
/recommendations/{recID}/detail

Get full detail for a single recommendation including metrics and remediation steps.

Refresh Recommendations

POST
/recommendations/refresh

Trigger a recommendation recomputation for the organization.

Resource Intensive

Refreshing recommendations re-evaluates all rules against all resources. Use sparingly.

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.

GET
/recommendations/{recID}/workflow

Preview the workflow plan that would be created for this recommendation — steps, target spec, and approval gates — without persisting anything.

POST
/recommendations/{recID}/workflow

Start a remediation workflow for this recommendation. Returns a workflow job ID; poll /workflows/{jobID} for status.

GET
/workflows/pending-approval

List workflow steps across the org that are waiting on a human approver. Used by the approvals queue widget.

GET
/workflows/{jobID}

Get a workflow's status, current step, and per-step audit trail.

POST
/workflows/{jobID}/cancel

Cancel an in-flight workflow. Steps already applied are not rolled back; subsequent steps are skipped.

POST
/workflows/{jobID}/steps/{stepName}/approve

Approve 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.

POST
/workflows/{jobID}/steps/{stepName}/reject

Reject a paused workflow step. The workflow halts and no further steps are dispatched.

POST
/workflows/{jobID}/steps-by-id/{stepID}/approve

Approve a paused workflow step by its stable ID. Prefer this when the UI already has the step ID — it survives template renames.

POST
/workflows/{jobID}/steps-by-id/{stepID}/reject

Reject 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.

Scopetarget_typePriority (higher = more specific)
Organisationorganisation0 (lowest)
Cloud accountcloud_account1

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).

spec structurejson
{
  "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

KeyBehaviour
allAll child clauses must match (AND). Empty array → matches everything.
anyAt least one child clause must match (OR). Empty array → matches nothing.
notInverts a single child clause.
(absent / {})No selector → matches every finding (catch-all policy).

Selector leaf attributes

attrValid values
categoryidle, rightsizing, schedule, orphan, compliance, discount, governance
severitycritical, high, medium, low, info
resourceTypeAny resource type string (ec2, rds, disk, …) — use the catalog endpoint to get the full list

Fail-open on empty, fail-closed on malformed

An absent or empty 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

GET
/recommendations/policies/catalog

Returns 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.

Responsejson
{
  "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

The config store accepts any JSON spec without inspecting it. Call this endpoint first — it checks that the selector uses known attributes and operators, and that the notify block contains at least one channel. The form gates Create on valid: true.
POST
/recommendations/policies/validations

Stateless pre-create validation. Returns { valid, errors } and persists nothing.

Requestbash
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"] }
    }
  }'
Response — validjson
{ "data": { "valid": true, "errors": [] } }
Response — invalidjson
{
  "data": {
    "valid": false,
    "errors": [
      { "field": "spec.notify.channels", "message": "at least one channel is required" }
    ]
  }
}

List policies

GET
/policies

List the organisation's recommendation policies (paginated). Pass domain=recommendation to scope to this feature.

Responsejson
{
  "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

POST
/policies

Create a notification policy. Validate the spec with the validations endpoint first.

Requestbash
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"]
      }
    }
  }'
Responsejson
{
  "data": {
    "id":         "pol_abc123",
    "domain":     "recommendation",
    "targetType": "organisation",
    "targetId":   "org_xyz",
    "name":       "Critical findings → Slack",
    "enabled":    true,
    "createdAt":  "2025-01-15T10:30:00Z"
  }
}
GET
/policies/{id}

Get a single policy including its full spec.

PATCH
/policies/{id}

Update a policy's name, description, spec, or enabled flag.

Request — disable a policybash
curl -X PATCH https://zopnight.com/api/policies/pol_abc123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
DELETE
/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.