---
title: "Update an SLO"
method: PUT
path: "/api/{org_id}/slos/{slo_id}"
tags: ["SLOs"]
---

# Update an SLO

`PUT /api/{org_id}/slos/{slo_id}`

## Request

- Base URL: `https://api.openobserve.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.openobserve.ai/api/{org_id}/slos/{slo_id}`
- Auth: API key in header `Authorization`
- TypeScript: <https://skmtc.dev/openobserve/apis/openobserve-api-http/docs/api/:org_id/slos/:slo_id/put.ts> (a function with typed inputs; its response is checked against the schema)

## Path parameters

- `org_id` string, required
- `slo_id` string, required

## Request body

- union
  - object: A Service Level Objective (S-1).
    - `config` object, required
      - `source` union, required: How a count SLI obtains good/total — itself a tagged union so that unrepresentable states cannot be constructed (D40). **Adjacently tagged, not internally tagged.** This workspace builds `serde_json` with `arbitrary_precision` (root `Cargo.toml`), which encodes every number as a map. Internally-tagged enums buffer their whole subtree through `serde::__private::de::Content` before dispatching, and that buffered map cannot then be deserialized into an `f64` — the failure is `invalid type: map, expected f64`, and it survives nesting. Serialization still succeeds, so the bug only appears on read-back. This variant carries no numbers *today*, but the dual-query form is the one most likely to gain them, and the failure mode is silent enough to be worth pre-empting. See [`SliConfig`] for the same treatment and a regression test.
        - object: The native form: one scan, so good and total are provably from the same rows.
          - `mode` 'single_query', required
          - `query` object, required: The native form: one scan, so good and total are provably from the same rows.
            - `good_expr` string, required: Numerator predicate. Mandatory — in this arm there is no other definition of `good`.
            - `scope` string, nullable: Denominator filter. `None` = all rows.
            - `stream` string, required
            - `stream_type` string, required
        - object: Importer-only fallback for an unfoldable imported pair. Weaker atomicity: two scans that cannot be proven to have seen the same instant.
          - `mode` 'dual_query', required
          - `query` object, required: Importer-only fallback for an unfoldable imported pair. Weaker atomicity: two scans that cannot be proven to have seen the same instant.
            - `good` CountQuery, required: One query of a dual-query count source. Each carries its **own** stream — D40's fallback exists precisely for numerator/denominator pairs that do not share one.
              - …
            - `total` CountQuery, required: One query of a dual-query count source. Each carries its **own** stream — D40's fallback exists precisely for numerator/denominator pairs that do not share one.
              - …
        - object: Metrics-native counting: two PromQL expressions, each evaluated per slice (wire tag `prom_ql`, following [`QueryLanguage::PromQl`]). Exists because pre-aggregated counters have no rows for a `good_expr` to classify — "good" only exists as arithmetic between series, and correct counter arithmetic is `increase()` (monotonic-reset-aware), which SQL over raw samples cannot express. The expressions should use a range selector equal to the slice interval (`increase(http_requests_total[5m])` for a 5-minute slice); the evaluator samples them at slice ends, so each sample covers exactly its slice. Same atomicity caveat as [`Self::DualQuery`]: two evaluations that cannot be proven to have seen the same instant. Grouping needs no column list — the returned series' labels supply the group values.
          - `mode` 'prom_ql', required
          - `query` object, required: Metrics-native counting: two PromQL expressions, each evaluated per slice (wire tag `prom_ql`, following [`QueryLanguage::PromQl`]). Exists because pre-aggregated counters have no rows for a `good_expr` to classify — "good" only exists as arithmetic between series, and correct counter arithmetic is `increase()` (monotonic-reset-aware), which SQL over raw samples cannot express. The expressions should use a range selector equal to the slice interval (`increase(http_requests_total[5m])` for a 5-minute slice); the evaluator samples them at slice ends, so each sample covers exactly its slice. Same atomicity caveat as [`Self::DualQuery`]: two evaluations that cannot be proven to have seen the same instant. Grouping needs no column list — the returned series' labels supply the group values.
            - `good` string, required
            - `total` string, required
    - `sli_type` 'count', required
    - `group_by` string[], nullable: THE canonical location for grouping.
    - `slice_interval_secs` integer, required
    - `window_secs` integer, required
    - `definition_generation` integer: Bumped by every computation-affecting edit, including reverts (D59). Also the writing epoch and the CAS fence for writer commits.
    - `description` string
    - `enabled` boolean
    - `folder_id` string
    - `groups_estimate` integer, nullable: Preflight `COUNT(DISTINCT …)` estimate; feeds the S-10 cap and the S-14 budget.
    - `groups_reserved` integer: Budget reservation: 1 when ungrouped, else `clamp(2 × estimate, 64, hard cap)`.
    - `id` string
    - `name` string, required
    - `org` string
    - `owner` string, nullable
    - `tags` string[]
    - `target` number, double, required: Percentage in (0, 100), up to 3 decimals. Applied at READ time.
  - object: A Service Level Objective (S-1).
    - `config` object, required
      - `absent_is_bad` boolean: Freshness semantics: a slice the query PROVED empty is **bad** rather than a gap. For a pipeline-freshness SLO, absence is the failure signal — a silent pipeline is a broken pipeline, and under S-8's default it could never read as bad. Only flips the meaning of a *successful* query's empty bucket; a FAILED query still writes nothing and coverage falls, so a search outage freezes the SLO exactly as before. That distinction is structural — gap fill runs only after a successful query. `#[serde(default)]` is the upgrade guarantee: every stored time-slice SLO predates the field and keeps S-8 byte-for-byte. Ungrouped SLOs only (validated) — gap fill cannot see a group that is absent from the whole pass, so a grouped freshness SLO would freeze instead of firing for precisely the failure it watches for.
      - `comparator` '=' | '!=' | '>' | '>=' | '<' | '<=' | 'contains' | 'not_contains' | 'is_null' | 'is_not_null' | 'is_empty' | 'is_not_empty', required
      - `query` string, required
      - `query_language` 'sql' | 'prom_ql', required: Query language for a time-slice aggregate. Carried explicitly rather than inferred from the stream type, so the evaluator never guesses.
      - `scope` string, nullable
      - `stream` string, required
      - `stream_type` string, required
      - `threshold` number, double, required
    - `sli_type` 'time_slice', required
    - `group_by` string[], nullable: THE canonical location for grouping.
    - `slice_interval_secs` integer, required
    - `window_secs` integer, required
    - `definition_generation` integer: Bumped by every computation-affecting edit, including reverts (D59). Also the writing epoch and the CAS fence for writer commits.
    - `description` string
    - `enabled` boolean
    - `folder_id` string
    - `groups_estimate` integer, nullable: Preflight `COUNT(DISTINCT …)` estimate; feeds the S-10 cap and the S-14 budget.
    - `groups_reserved` integer: Budget reservation: 1 when ungrouped, else `clamp(2 × estimate, 64, hard cap)`.
    - `id` string
    - `name` string, required
    - `org` string
    - `owner` string, nullable
    - `tags` string[]
    - `target` number, double, required: Percentage in (0, 100), up to 3 decimals. Applied at READ time.
  - object: A Service Level Objective (S-1).
    - `config` object, required
      - `alert_id` string, required
    - `sli_type` 'alert', required
    - `group_by` string[], nullable: THE canonical location for grouping.
    - `slice_interval_secs` integer, required
    - `window_secs` integer, required
    - `definition_generation` integer: Bumped by every computation-affecting edit, including reverts (D59). Also the writing epoch and the CAS fence for writer commits.
    - `description` string
    - `enabled` boolean
    - `folder_id` string
    - `groups_estimate` integer, nullable: Preflight `COUNT(DISTINCT …)` estimate; feeds the S-10 cap and the S-14 budget.
    - `groups_reserved` integer: Budget reservation: 1 when ungrouped, else `clamp(2 × estimate, 64, hard cap)`.
    - `id` string
    - `name` string, required
    - `org` string
    - `owner` string, nullable
    - `tags` string[]
    - `target` number, double, required: Percentage in (0, 100), up to 3 decimals. Applied at READ time.

## Response `200`

Updated

- HttpResponse: HTTP response code 200 is success code 400 is error code 404 is not found code 500 is internal server error code 503 is service unavailable code >= 1000 is custom error code message is the message or error message
  - `code` integer, required
  - `error_detail` string, nullable
  - `hint` string, nullable: One-line guidance on how to fix the error (agent self-correction, D4).
  - `id` string, nullable
  - `message` string, required
  - `name` string, nullable
  - `suggestions` string[], nullable: Closest valid alternatives for a mistyped field/function name.
  - `trace_id` string, nullable
  - `warnings` string[], nullable: Problems with a request that succeeded anyway. Distinct from `hint`, which explains a failure: these accompany a 200, and the caller is entitled to have meant it.

## Other responses

- `400`: Bad Request
- `403`: Forbidden

---

[API](https://skmtc.dev/openobserve/apis/openobserve-api-http.md) - [All operations](https://skmtc.dev/openobserve/apis/openobserve-api-http/llms.txt) - [OpenAPI document](https://skmtc.dev/openobserve/apis/openobserve-api-http/revisions/0529b8a10252?raw)
