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

# Assemble rules into a scored decision

> **Beta.** The request and response shapes may still change.

Assemble rules into a scored decision: each rule carries a weight, the weights of the rules that trigger are summed, and the recipe flags when the total reaches the threshold.

Every rule associated here is read and compiled against this recipe's own environment - the attributes and parameters it declares. A rule that does not exist, or that references a name this recipe does not declare, is refused with the offending `rules.N.rule_id` in `details`.

A recipe is where rules become a verdict. Each rule carries a **weight**; the weights of the rules that trigger are summed; the recipe flags when the total reaches the **threshold**.

```json theme={null}
{
  "name": "signup_proxy_abuse",
  "status": "SHADOW",
  "threshold": 70,
  "rules": [
    { "rule_id": "rul_01jc0t6fwwfgfsq1md24mhyztj", "weight": 40 },
    { "rule_id": "rul_01jd1u7gxxghgtr2ne35nizauk", "weight": 30 },
    { "rule_id": "rul_01je2v8hyyhihus3of46ojabvl", "weight": 50, "preempts": "FLAG" }
  ],
  "attributes": ["plan_tier"],
  "parameters": { "blocked_countries": "RU,BY" }
}
```

## Start in `SHADOW`

A threshold set without evidence is a guess. `SHADOW` evaluates against live traffic and reports its verdict without it counting toward the evaluation-level answer, which is the only way to see what a threshold would have done before it decides anything. Move to `LIVE` when the numbers justify it.

`DRAFT` and `INACTIVE` are the two ways a recipe exists without being in service - one not yet put into it, one taken back out. A flow referencing either runs its other recipes and reports nothing for that one: it wasn't asked, which is not the same as having passed.

## Preempting rules

`preempts` makes a rule's outcome the recipe's verdict, bypassing the score. Use it for a condition that is conclusive on its own.

Expressing that through weights instead means picking a number above every threshold the recipe might later be given - a fact about the recipe's arithmetic rather than about the rule, which quietly stops being true when the threshold moves.

The weight still applies and the score is still reported. In an evaluation, `determined_by` references the preempting rule; it is the only thing that accounts for a recipe reporting a score under its threshold and flagging anyway.

## Attributes and parameters

Both let a rule read something the signal catalog doesn't know. They differ in how long a value lasts, and that difference decides everything else:

|               | `attributes`                                              | `parameters`                                       |
| ------------- | --------------------------------------------------------- | -------------------------------------------------- |
| Referenced as | `attr.<key>`                                              | `param.<key>`                                      |
| Value arrives | With each evaluation, in the request                      | Here, when the recipe is written                   |
| Changes       | Per request                                               | Only when the recipe is replaced                   |
| If absent     | Missing evidence: rules reading it report `NOT_EVALUATED` | Cannot be absent - the recipe would not have saved |
| Good for      | Plan tier, account age, cart total                        | A country list, a limit, a tuning constant         |

Keys are lower snake\_case, at most 64 characters, declared without their namespace. At most 32 of each. Values are strings - the only type either supports today.

## What is validated

Every rule associated here is read and compiled **in this recipe's environment**, which is what catches a mismatch between a rule and the recipe using it:

* a rule identifier that doesn't exist, or belongs to no tier you can reach
* a rule reading `attr.` or `param.` names this recipe doesn't declare
* a **managed** rule reading any `attr.` or `param.` name at all - Prelude's rules read signals only, and one pinned to your recipe would stop being shared

Failures come back together, one `details` entry per rule, keyed `rules.N.rule_id` - so a recipe with three problems takes one round trip to fix, not three. A managed rule that won't compile is reported as unusable without quoting its expression.


## OpenAPI

````yaml post /v2/watch/management/recipes
openapi: 3.1.0
info:
  title: Prelude API
  version: 2.0.0
  description: The Prelude API allows you to send messages to your users.
  contact:
    email: support@prelude.so
servers:
  - url: https://api.prelude.dev
    description: Production server
security:
  - apiToken: []
tags:
  - name: Notify
    description: Send transactional and marketing messages with compliance enforcement.
  - name: Transactional
    description: Send transactional messages (deprecated - use Notify API instead).
  - name: Verify
    description: Verify phone numbers.
  - name: Watch
    description: Evaluate email addresses and phone numbers for trustworthiness.
  - name: Lookup
    description: >-
      Retrieve detailed information about a phone number including carrier data,
      line type, and portability status.
paths:
  /v2/watch/management/recipes:
    post:
      tags:
        - Watch
      summary: Create a recipe
      description: >-
        **Beta.** The request and response shapes may still change.


        Assemble rules into a scored decision: each rule carries a weight, the
        weights of the rules that trigger are summed, and the recipe flags when
        the total reaches the threshold.


        Every rule associated here is read and compiled against this recipe's
        own environment - the attributes and parameters it declares. A rule that
        does not exist, or that references a name this recipe does not declare,
        is refused with the offending `rules.N.rule_id` in `details`.
      operationId: createWatchRecipe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WatchRecipeRequest'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WatchRecipe'
        '400':
          description: KO
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                example:
                  code: invalid_recipe
                  message: >-
                    The provided recipe is invalid. See `details` field for more
                    information.
                  type: bad_request
                  param: recipe
                  details:
                    - path: status
                      message: >-
                        The status must be one of 'DRAFT', 'SHADOW', 'LIVE', or
                        'INACTIVE'.
                    - path: rules.0.rule_id
                      message: A rule referenced here does not exist.
                  request_id: 3d19215e-2991-4a05-a41a-527314e6ff6a
components:
  schemas:
    WatchRecipeRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 128
          examples:
            - signup_proxy_abuse
        status:
          type: string
          enum:
            - DRAFT
            - SHADOW
            - LIVE
            - INACTIVE
          description: >
            Whether the recipe takes part in an evaluation, and whether its
            verdict counts.
             * `DRAFT` - Not yet in service. It does not evaluate.
             * `SHADOW` - Evaluates and is reported, so you can watch a threshold against live traffic before trusting it.
             * `LIVE` - Evaluates, and its verdict counts toward the evaluation-level answer.
             * `INACTIVE` - Taken back out of service. It does not evaluate.
          examples:
            - SHADOW
        threshold:
          type: integer
          format: int64
          description: >-
            The score at which the recipe flags. The weights of the rules that
            triggered are summed, and the recipe flags when the total reaches
            this value. A threshold of `0` flags every evaluation.
          examples:
            - 70
        rules:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WatchRecipeRule'
          description: >-
            The rules this recipe scores over, with their weights. At least one
            is required.
        attributes:
          type: array
          items:
            type: string
          description: >
            The request-level facts this recipe's rules may read, declared by
            key and referenced in an expression as `attr.<key>`. Values arrive
            with each evaluation.


            A key is lower snake_case, at most 64 characters, and at most 32 may
            be declared. An attribute a rule reads and a recipe does not declare
            is a compile error, reported against that rule.
          examples:
            - - plan_tier
              - account_age_days
        parameters:
          type: object
          additionalProperties:
            type: string
          description: >
            This recipe's configuration: keys and their values, referenced in an
            expression as `param.<key>`, fixed until the recipe is replaced. Use
            them for the country list or the limit a shared rule should apply
            here, so the same rule can serve two recipes tuned differently.


            Keys follow the same format as attributes, and at most 32 may be
            declared. Values are strings - the only type either supports today.
          examples:
            - blocked_countries: RU,BY
              max_attempts: '5'
      required:
        - name
        - status
        - threshold
        - rules
    WatchRecipe:
      type: object
      properties:
        id:
          type: string
          examples:
            - rcp_01jc0t6fwwfgfsq1md24mhyztj
          description: The recipe identifier. Reference it in a flow's `recipes` to run it.
        name:
          type: string
          examples:
            - signup_proxy_abuse
        catalog_version:
          type: integer
          format: int64
          description: >-
            The version of the Prelude signal catalog this recipe's rules are
            compiled against, stamped by the server when the recipe is created
            or replaced. It is what stops a catalog that later grows or
            deprecates entries from silently changing what this recipe means.
          examples:
            - 1
        status:
          type: string
          enum:
            - DRAFT
            - SHADOW
            - LIVE
            - INACTIVE
        threshold:
          type: integer
          format: int64
          examples:
            - 70
        rules:
          type: array
          items:
            $ref: '#/components/schemas/WatchRecipeRule'
        attributes:
          type: array
          items:
            type: string
          examples:
            - - plan_tier
              - account_age_days
        parameters:
          type: object
          additionalProperties:
            type: string
          examples:
            - blocked_countries: RU,BY
              max_attempts: '5'
      required:
        - id
        - name
        - catalog_version
        - status
        - threshold
        - rules
    Error:
      type: object
      properties:
        code:
          type: string
          description: The error code.
          examples:
            - invalid_phone_number
        message:
          type: string
          examples:
            - >-
              The provided phone number is invalid. Provide a valid E.164 phone
              number.
          description: A human-readable message describing the error.
        type:
          type: string
          examples:
            - bad_request
          description: The error type.
        request_id:
          type: string
          examples:
            - 3d19215e-2991-4a05-a41a-527314e6ff6a
          description: >-
            A string that identifies this specific request. Report it back to us
            to help us diagnose your issues.
        param:
          type: string
          examples:
            - phone_number
          description: The parameter the error refers to, when it applies to a single one.
      required:
        - code
        - message
        - type
    WatchRecipeRule:
      type: object
      description: >-
        One rule's place in a recipe - what it contributes to the score, and
        whether it can settle the verdict on its own.
      properties:
        rule_id:
          type: string
          examples:
            - rul_01jc0t6fwwfgfsq1md24mhyztj
          description: A rule you authored, or one from the managed listing.
        weight:
          type: integer
          format: int64
          examples:
            - 40
          description: >-
            What this rule adds to the recipe's score when it triggers. A rule
            that does not trigger, or that could not run, adds nothing.
        preempts:
          type: string
          enum:
            - PASS
            - FLAG
          description: >
            Makes this rule's outcome the recipe's verdict when it triggers,
            bypassing the score. Omit it in the ordinary case, where the rule
            contributes its weight and the total decides.


            It exists because a weighted score cannot express a certainty:
            saying "this alone is conclusive" through the weights means picking
            a number above every threshold the recipe might later be given,
            which stops being true the moment the threshold moves. The weight
            still applies and the score is still reported - a preempting rule
            overrides the conclusion, it does not erase the evidence.
          examples:
            - FLAG
      required:
        - rule_id
        - weight
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer

````