> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scanoss.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a policy

> Creates a new policy. The expression is compiled via `cel-go` before persistence; every `params.X` reference MUST resolve to a `parameter_spec` entry. Typical client flow: fetch the templates catalog via `GET /v1/policies/templates`, pick a seeded template, POST a body sourced from that row (the "Add from template" UX in the web client).

Supplying `attach_project_id` makes this one atomic create-and-attach: a 201 means the policy exists AND is attached to that project, and any failure leaves neither. There is no separate attach call to lose, and therefore no orphan definition to clean up — which matters because a policy is a shared organization-wide resource the client cannot delete on its own behalf.

`Idempotency-Key` makes the whole operation repeatable, which is what a retry after a lost response needs: without it, the retry creates a second shared definition.



## OpenAPI

````yaml /api-reference/earnie/policies-openapi.json post /v1/policies
openapi: 3.0.3
info:
  title: Earnie Policies API
  description: >-
    Author policies, attach them to projects, and read how projects, scans, and
    findings evaluate against them.
  version: 1.0.0
servers:
  - url: https://{tenant}.earnie.dev
    description: Your Earnie deployment
    variables:
      tenant:
        default: your-company
        description: >-
          Your organization's Earnie address, as shown in your browser when you
          sign in.
security:
  - bearerAuth: []
paths:
  /v1/policies:
    post:
      summary: Create a policy
      description: >-
        Creates a new policy. The expression is compiled via `cel-go` before
        persistence; every `params.X` reference MUST resolve to a
        `parameter_spec` entry. Typical client flow: fetch the templates catalog
        via `GET /v1/policies/templates`, pick a seeded template, POST a body
        sourced from that row (the "Add from template" UX in the web client).


        Supplying `attach_project_id` makes this one atomic create-and-attach: a
        201 means the policy exists AND is attached to that project, and any
        failure leaves neither. There is no separate attach call to lose, and
        therefore no orphan definition to clean up — which matters because a
        policy is a shared organization-wide resource the client cannot delete
        on its own behalf.


        `Idempotency-Key` makes the whole operation repeatable, which is what a
        retry after a lost response needs: without it, the retry creates a
        second shared definition.
      operationId: CreatePolicy
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Opaque client-generated key. Repeating the same request with the
            same key and actor returns the originally created policy (201, no
            second policy); reusing the key for a DIFFERENT request returns `409
            idempotency_conflict`.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePolicyRequest'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Policy'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CELCompileError'
                  - $ref: '#/components/schemas/ParamSpecMissingError'
                  - $ref: '#/components/schemas/UnknownFieldPathError'
                  - $ref: '#/components/schemas/ValuesInvalidError'
        '403':
          description: >-
            `project_out_of_scope` — `attach_project_id` names a project outside
            the API key's project scope. Attaching a policy to a project starts
            gating that project, so it is refused exactly as `PUT
            /v1/projects/{id}/policies/{id}` is. Nothing is written.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            `attach_project_id` names no project in the caller's organization.
            Nothing is written.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            Slug already taken, or (`idempotency_conflict`) the supplied
            `Idempotency-Key` was already used by this actor for a different
            request.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SlugTakenError'
                  - $ref: '#/components/schemas/Error'
        '422':
          description: >-
            Unguarded field access, a policy `module` that does not own the
            referenced fields, or a scanner the organization has not enabled.
            The last answers with the `no_fixture` shape on purpose: naming the
            withheld scanner would itself advertise it.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/UnguardedFieldError'
                  - $ref: '#/components/schemas/ModuleMismatchError'
                  - $ref: '#/components/schemas/NoFixtureError'
components:
  schemas:
    CreatePolicyRequest:
      type: object
      required:
        - module
        - category
        - name
        - action
        - expression
        - parameter_spec
        - values
      properties:
        slug:
          type: string
          description: >-
            Optional. When omitted, the server derives a slug from `name`
            (kebab-cased) and disambiguates collisions. Supply an explicit slug
            only when migrating known-identity rows.
        module:
          type: string
        category:
          type: string
        name:
          type: string
        description:
          type: string
        agent_rule:
          type: string
          description: >-
            The imperative sentence an AI coding agent reads instead of the CEL
            (see `Policy.agent_rule`). Optional; omitted means the rendering
            falls back to `description`.
        enabled:
          type: boolean
          default: true
        action:
          $ref: '#/components/schemas/Action'
        expression:
          type: string
        parameter_spec:
          type: array
          items:
            $ref: '#/components/schemas/ParameterSpec'
        values:
          type: object
          additionalProperties: true
        attach_project_id:
          type: string
          format: uuid
          description: >-
            Optional. When present, the created policy is attached to this
            project in the SAME transaction and authorization context as its
            creation — a failed attachment rolls the creation back rather than
            leaving an orphan definition behind.


            Omit it for organization-level authoring, which stays valid for the
            consumers that deliberately want a definition with no attachment
            yet. The project must belong to the caller's organization; one that
            does not refuses the whole request before anything is written.
    Policy:
      type: object
      required:
        - id
        - slug
        - source
        - module
        - category
        - name
        - description
        - agent_rule
        - agent_rule_stale
        - enabled
        - action
        - expression
        - parameter_spec
        - values
        - revision
        - created_at
        - updated_at
      properties:
        id:
          type: integer
          format: int64
          readOnly: true
        slug:
          type: string
          description: >-
            Stable, human-readable identifier safe across deploys and
            migrations. Generated from `name` (kebab-cased) on Create when not
            supplied; templates ship with deterministic slugs (e.g.
            `block-copyleft-licenses`). Immutable after creation.
        source:
          allOf:
            - $ref: '#/components/schemas/PolicySource'
          readOnly: true
          description: >-
            Server-managed origin of the policy. Seeded templates return `seed`;
            user-authored rows return `custom`.
        module:
          type: string
          description: >-
            Finding domain the policy targets (e.g. `scans`, `crypto`,
            `crypto_finder`). AI provenance is an xBOM enrichment, not a
            separate policy domain.
        category:
          type: string
          description: Grouping within a scanner (e.g. `licenses`, `vulnerabilities`).
        name:
          type: string
        description:
          type: string
          description: Human-readable explanation of what the policy checks and why.
        agent_rule:
          type: string
          description: >-
            Keep it to roughly 200 bytes. That is not a stored-length limit —
            nothing rejects a longer one — it is where the RENDERED sentence is
            truncated with an ellipsis before it reaches an agent, measured
            after placeholders are substituted and in bytes rather than
            characters. A short rule whose `{banned_licenses}` expands to three
            hundred bytes is truncated too. The cap exists because this text
            sits in every prompt of every session.


            Written in the second person, one sentence, no CEL: "Do not add
            dependencies licensed {banned_licenses}."


            `{key}` placeholders name `parameter_spec[].key` entries and are
            substituted with the policy's current `values` at render time, so
            one sentence stays true after a parameter is tuned. A list value
            renders as `a, b or c`. A placeholder naming no parameter is
            rejected on save.


            Empty is allowed and means "no agent rule": rendering falls back to
            `description`, and last to the CEL expression labelled as such, so a
            policy without one still reaches an agent. Like `description` it is
            prose and NOT part of the policy `revision` — rewording what an
            agent reads does not change what the gate does.
        agent_rule_stale:
          type: boolean
          readOnly: true
          description: >-
            True when `agent_rule` was written against different rule logic than
            this policy now carries — its `action`, `expression`, or the set of
            parameter keys has changed since the sentence was last saved.


            An agent rule is hand-written prose and cannot be derived from CEL,
            so nothing keeps it true when the expression moves underneath it.
            This is how the product notices: the sentence still renders (it is
            usually still mostly right), but the policy is flagged so a human
            can review it. Saving the agent rule again clears the flag.


            Parameter VALUES are deliberately excluded: `{key}` placeholders
            already keep the sentence true across a retune, so a threshold tweak
            never raises this. False when the policy has no agent rule, and
            false for a row that predates the stored basis — "unknown" must not
            read as "stale".
        enabled:
          type: boolean
        action:
          $ref: '#/components/schemas/Action'
        expression:
          type: string
          description: >-
            CEL string. Compiled via `cel-go` on save (parse + type check +
            params.X consistency) and evaluated against findings via `Eval` (see
            `/preview` for the live-evaluation surface).
        parameter_spec:
          type: array
          items:
            $ref: '#/components/schemas/ParameterSpec'
        values:
          type: object
          additionalProperties: true
          description: >-
            Current parameter values keyed by `parameter_spec[].key`. Every
            declared key needs a value or a `default` in its spec entry,
            including a parameter the expression only reads behind `has()`; a
            key left out is stored with its `default`. See `ValuesInvalidError`.
        revision:
          type: string
          readOnly: true
          description: >-
            The policy's immutable content revision: a `sha256:` digest over the
            canonical form of the gating content — `action`, `expression`,
            `parameter_spec`, `values`. It is byte-identical to the
            `policy_hash` an evaluation records and to the revision a scan's
            policy snapshot names, so "which revision produced this verdict" and
            "which revision is live now" are answered in one vocabulary.


            Mutations echo it back in `If-Match`. A stale revision is `409
            policy_conflict`; a policy edit is never last-write-wins.


            It covers the gating content and nothing else. Renaming a policy or
            rewriting its description leaves the revision unchanged, because
            neither changes what the gate does, and `enabled` is carried by
            presence in a scan's snapshot rather than by the digest.
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
        result_summary:
          $ref: '#/components/schemas/PolicyResultSummary'
    CELCompileError:
      type: object
      required:
        - code
        - message
        - position
      description: >-
        Returned when `expression` does not parse / does not type-check under
        `cel-go`. Code is `cel_compile`.
      properties:
        code:
          type: string
          enum:
            - cel_compile
        message:
          type: string
        position:
          type: integer
          description: 0-based character offset of the first syntax/type error.
    ParamSpecMissingError:
      type: object
      required:
        - code
        - key
      description: >-
        Returned when the expression references `params.X` for a key `X` that
        has no matching entry in `parameter_spec`. Code is `param_spec_missing`.
      properties:
        code:
          type: string
          enum:
            - param_spec_missing
        key:
          type: string
    UnknownFieldPathError:
      type: object
      required:
        - code
        - path
      description: >-
        Returned when `expression` references a `finding.*` selector path (or a
        comprehension-bound variable's member access) that does not resolve
        against the field catalog. Code is `unknown_field_path`.
      properties:
        code:
          type: string
          enum:
            - unknown_field_path
        path:
          type: string
        suggestion:
          type: string
          description: Nearest registered path by edit distance, when close enough.
    ValuesInvalidError:
      type: object
      required:
        - code
        - key
        - message
      description: >-
        Returned when a policy's `values` do not fit its `parameter_spec`: a key
        the spec does not declare, a declared key with neither a value nor a
        `default` (the author must add one or the other), a value or a `default`
        of the wrong type, a choice outside the parameter's `options`, or a
        parameter whose `type` is not a `ParameterType`. Checked on create and
        clone, and on update whenever `values` or `parameter_spec` change. A
        declared key left out of `values` is stored with its `default`. Code is
        `values_invalid`.
      properties:
        code:
          type: string
          enum:
            - values_invalid
        key:
          type: string
          description: The parameter key whose value was refused.
        message:
          type: string
          description: Why the value was refused, naming the key.
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
    SlugTakenError:
      type: object
      required:
        - code
        - slug
      description: >-
        Returned by `POST /v1/policies` when the supplied `slug` collides with
        an existing row. Code is `slug_taken`.
      properties:
        code:
          type: string
          enum:
            - slug_taken
        slug:
          type: string
    UnguardedFieldError:
      type: object
      required:
        - code
        - message
        - path
        - optional
        - position
      description: >-
        Returned when `expression` reads a field that may be absent on an
        individual finding without a dominating `has()` guard. The expression
        compiles and type-checks — only the catalog knows the access is unsafe,
        which is why this is 422 and not 400. Code is `unguarded_field`.


        Without the guard the access throws at evaluation for exactly the
        findings the author was gating for, and `&&` cannot absorb it, so the
        gate fails open instead of matching.
      properties:
        code:
          type: string
          enum:
            - unguarded_field
        message:
          type: string
        path:
          type: string
          description: The unguarded field path, e.g. `finding.crypto.key_size`.
        optional:
          type: boolean
          description: >-
            True for an intrinsic optional field (all `crypto.*`) — add
            `has(<path>)`. False for an enrichment-backed field declaring
            `requires` — a guard on the carrier is also accepted there.
        position:
          type: integer
          description: 0-based column of the offending selector.
    ModuleMismatchError:
      type: object
      required:
        - code
        - message
        - module
        - expected
        - path
      description: >-
        Returned when a policy's declared `module` does not own the vocabulary
        its `expression` reads — a rule over `finding.crypto.*` filed under
        module `oss`. Code is `module_mismatch`.


        `module` is not bookkeeping: it selects the preview fixture corpus,
        drives entitlement, and is read back to the user as "Evaluates <module>
        findings". A mismatch is refused rather than silently corrected, because
        either half may be the one the caller meant.
      properties:
        code:
          type: string
          enum:
            - module_mismatch
        message:
          type: string
        module:
          type: string
          description: The module the caller declared.
        expected:
          type: string
          description: The module that owns the referenced field.
        path:
          type: string
          description: One referenced field the declared module does not own.
    NoFixtureError:
      type: object
      required:
        - code
      description: >-
        Returned by `/preview` when no findings fixture is committed for the
        requested module. Code is `no_fixture`.
      properties:
        code:
          type: string
          enum:
            - no_fixture
        module:
          type: string
    Action:
      type: string
      enum:
        - block
        - warn
        - require
      description: >-
        Enforcement action applied when the policy fires. Verdict mapping: block
        → block warn → warn require → require
    ParameterSpec:
      type: object
      required:
        - key
        - label
        - type
      properties:
        key:
          type: string
          description: >-
            Semantic identifier (e.g. `min_stars`, `blocked_severities`).
            Referenced inside the expression as `params.<key>`.
        label:
          type: string
        type:
          $ref: '#/components/schemas/ParameterType'
        options:
          type: array
          items:
            type: string
          nullable: true
        default:
          description: >-
            Used as the value when `values` leaves this key out, and stored as
            such. Must itself fit `type` and `options`. A parameter with no
            `default` must always be given a value.
        constrains:
          type: string
          nullable: true
          description: >-
            Optional field-catalog `path` this parameter's values should be
            drawn from (e.g. `license.spdx_id`). Descriptive metadata only — NOT
            validated against the catalog at save time; consumed by frontend
            parameter pickers (out of scope for this change) to reuse `GET
            /fields/{path}/values`.
    PolicySource:
      type: string
      enum:
        - seed
        - custom
      description: >-
        Server-managed provenance of the policy row. seed → curated template
        from the embedded manifest custom → user-authored policy created through
        the API
    PolicyResultSummary:
      type: object
      required:
        - state
        - violations_count
        - affected_files_count
        - affected_components_count
      properties:
        state:
          $ref: '#/components/schemas/EvaluationState'
        violations_count:
          type: integer
          minimum: 0
        affected_files_count:
          type: integer
          minimum: 0
          description: >-
            Distinct non-empty finding locators among current, non-approved
            violations.
        affected_components_count:
          type: integer
          minimum: 0
          description: >-
            Distinct non-empty component identifiers among current, non-approved
            violations.
        missing_enrichments:
          type: array
          items:
            type: string
          description: >-
            Canonical enrichment identifiers required before evaluation can
            complete.
        error_message:
          type: string
        last_evaluated_at:
          type: string
          format: date-time
    ParameterType:
      type: string
      enum:
        - string
        - int
        - float
        - bool
        - enum
        - enum_set
        - tag_list
      description: >-
        Runtime type of a parameter slot. Drives the UI input control and
        validation hints.
    EvaluationState:
      type: string
      enum:
        - passed
        - violated
        - not_evaluated
        - error
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An Earnie API key, sent as `Authorization: Bearer sk_earnie_...`. Create
        one under Settings > API keys. The key's scopes decide which operations
        it may call.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.