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

# Create, preview, and evaluate Ad Hoc Rules

> Create, preview, and evaluate Ad Hoc Rules programmatically for automated pipelines such as CI/CD.

export const MintTable = ({columns = [], rows = [], columnWidths = []}) => {
  const pushTextWithLineBreaks = (parts, text, keyBase) => {
    const segments = String(text).split(/\\n|\n/);
    segments.forEach((segment, idx) => {
      if (segment) {
        parts.push(<span key={`${keyBase}-text-${idx}`}>{segment}</span>);
      }
      if (idx < segments.length - 1) {
        parts.push(<br key={`${keyBase}-br-${idx}`} />);
      }
    });
  };
  const parseMarkdown = text => {
    if (text === null || text === undefined) return "";
    const str = String(text);
    const parts = [];
    let lastIndex = 0;
    const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g;
    let match;
    while (true) {
      match = pattern.exec(str);
      if (match === null) {
        break;
      }
      if (match.index > lastIndex) {
        pushTextWithLineBreaks(parts, str.substring(lastIndex, match.index), `before-${lastIndex}`);
      }
      const fullMatch = match[0];
      if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
        parts.push(<code key={match.index}>{fullMatch.slice(1, -1)}</code>);
      } else if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
        parts.push(<strong key={match.index}>{fullMatch.slice(2, -2)}</strong>);
      } else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
        parts.push(<em key={match.index}>{fullMatch.slice(1, -1)}</em>);
      } else if (fullMatch.startsWith("[")) {
        const linkText = match[2];
        const linkUrl = match[3];
        parts.push(<a key={match.index} href={linkUrl} className="text-black-600 dark:text-black-400">
            {linkText}
          </a>);
      }
      lastIndex = pattern.lastIndex;
    }
    if (lastIndex < str.length) {
      pushTextWithLineBreaks(parts, str.substring(lastIndex), `tail-${lastIndex}`);
    }
    if (parts.length > 0) {
      return parts;
    }
    const plainParts = [];
    pushTextWithLineBreaks(plainParts, str, "plain");
    return plainParts.length ? plainParts : str;
  };
  const safeColumns = Array.isArray(columns) ? columns : [];
  const safeRows = Array.isArray(rows) ? rows : [];
  const safeColumnWidths = Array.isArray(columnWidths) ? columnWidths : [];
  const hasColumnWidths = safeColumnWidths.some(w => w !== null && w !== undefined && w !== "");
  const toCssWidth = width => typeof width === "number" ? `${width}px` : String(width);
  const getColumnStyle = idx => {
    const rawWidth = safeColumnWidths[idx];
    if (rawWidth === null || rawWidth === undefined || rawWidth === "") {
      return undefined;
    }
    const width = toCssWidth(rawWidth);
    return {
      width,
      minWidth: width
    };
  };
  const containerStyle = hasColumnWidths ? undefined : {
    overflowX: "auto"
  };
  const tableStyle = hasColumnWidths ? {
    tableLayout: "fixed",
    width: "100%"
  } : {
    width: "max-content",
    minWidth: "100%"
  };
  if (!Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(columnWidths)) {
    console.warn("MintTable received invalid props:", {
      columns,
      rows,
      columnWidths
    });
  }
  if (!safeColumns.length && !safeRows.length) {
    return null;
  }
  return <div className="mint-table-container" style={containerStyle}>
      <table style={tableStyle}>
        {hasColumnWidths && <colgroup>
            {safeColumns.map((_, idx) => {
    const style = getColumnStyle(idx);
    return <col key={idx} style={style} />;
  })}
          </colgroup>}
        <thead>
          <tr>
            {safeColumns.map((col, idx) => <th key={idx} className="text-left" style={getColumnStyle(idx)}>
                <b>{parseMarkdown(col)}</b>
              </th>)}
          </tr>
        </thead>
        <tbody>
          {safeRows.map((row, rIdx) => {
    const safeRow = Array.isArray(row) ? row : [];
    return <tr key={rIdx}>
                {safeRow.map((cell, cIdx) => <td key={cIdx} style={getColumnStyle(cIdx)}>
                    {parseMarkdown(cell)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

After completing this workflow, you can use the API to create, preview, and evaluate an Ad Hoc Rule for automated workflows, such as CI/CD pipelines, and generate a Report and Annotations.

## Before you begin

* You have [a Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* You have either the ID of the Run to evaluate the Ad Hoc Rule against, or the IDs of the Assets and the time range to evaluate it over.

## How Ad Hoc Rules differ from Rules

An Ad Hoc Rule is a Rule intended for automated workflows such as CI/CD pipelines, where you create and evaluate the Rule in a single pipeline step.

* Setting `isExternal` to `true` when you create a Rule marks it as an Ad Hoc Rule instead of a Rule managed through the UI.
* Ad Hoc Rules don't appear in the Rules tab, unlike Rules that aren't Ad Hoc.
* Because of that, any Report generated from an Ad Hoc Rule displays the Rule's logic directly, so reviewers can see what was evaluated without looking it up elsewhere.
* Ad Hoc Rules are always immutable once created, since they're meant to be defined externally in source control and re-applied through your pipeline rather than edited in place. Rules that aren't Ad Hoc stay editable.

See [Rules vs Ad Hoc Rules](/documentation/reference/rule-settings#rules-vs-ad-hoc-rules) for the full comparison and when to use each.

<MintTable
  columns={['', '[BatchUpdateRules](#create-an-ad-hoc-rule)', '[EvaluateRulesPreview](#preview-an-ad-hoc-rule)', '[EvaluateRules](#evaluate-an-ad-hoc-rule)']}
  columnWidths={['10%', '30%', '30%', '30%']}
  rows={[
['**What it does**', 'Creates an Ad Hoc Rule', 'Performs a dry evaluation and returns the Annotations it would generate, without creating them', 'Evaluates the Ad Hoc Rule against a Run or Asset and creates a Report containing the generated Annotations'],
['**Scope**', 'Up to 1000 Rules per request', 'A Run only', 'A Run, or Assets with a time range'],
['**gRPC**', '[RuleService](/api/reference/protocol-buffers/rules)', '[RuleEvaluationService](/api/reference/protocol-buffers/rule_evaluation)', '[RuleEvaluationService](/api/reference/protocol-buffers/rule_evaluation)'],
]}
/>

<Info>
  **Other clients**: You can also create and evaluate Ad Hoc Rules using the official [Python](/api/reference/protocol-buffers#python), [Go](/api/reference/protocol-buffers#go), or [Rust](/api/reference/protocol-buffers#rust) client, or generate a client for another language with [Buf](/api/clients/generate-a-client-with-buf), since all of them use the same Protocol Buffers that back the REST API.
</Info>

## Create an Ad Hoc Rule

Call the [`BatchUpdateRules`](/api-reference/ruleservice/batchupdaterules) endpoint with `isExternal` set to `true`. Use `assetConfiguration` to scope the Rule to the Asset whose Channels the condition references.

<CodeGroup>
  ```bash curl theme={null}
  curl -X PUT -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" $SIFT_REST_URL/api/v1/rules:batchUpdate -d '{
    "rules": [
      {
        "name": "YOUR_AD_HOC_RULE_NAME",
        "description": "YOUR_AD_HOC_RULE_DESCRIPTION",
        "isExternal": true,
        "assetConfiguration": {
          "assetIds": ["YOUR_ASSET_ID"]
        },
        "conditions": [
          {
            "expression": {
              "calculatedChannel": {
                "expression": "$1 > 100",
                "channelReferences": {
                  "$1": { "name": "YOUR_CHANNEL_NAME" }
                }
              }
            },
            "actions": [
              {
                "actionType": "ANNOTATION",
                "configuration": {
                  "annotation": {
                    "annotationType": "ANNOTATION_TYPE_DATA_REVIEW"
                  }
                }
              }
            ]
          }
        ]
      }
    ],
    "overrideExpressionValidation": false
  }'
  ```

  ```python Python theme={null}
  # To run these examples, install the client: pip install sift-stack-py
  # These examples are written for sift-stack-py v0.19.1
  import os
  from dotenv import load_dotenv
  from sift_client import SiftClient
  from sift_client.sift_types.channel import ChannelReference
  from sift_client.sift_types.rule import RuleAction, RuleActionType, RuleAnnotationType, RuleCreate

  load_dotenv()

  client = SiftClient(
      api_key=os.getenv("SIFT_API_KEY"),
      grpc_url=os.getenv("SIFT_GRPC_URI"),
      rest_url=os.getenv("SIFT_REST_URI"),
  )

  rule = client.rules.create(
      RuleCreate(
          name="YOUR_AD_HOC_RULE_NAME",
          description="YOUR_AD_HOC_RULE_DESCRIPTION",
          is_external=True,
          asset_ids=["YOUR_ASSET_ID"],
          expression="$1 > 100",
          channel_references=[
              ChannelReference(channel_reference="$1", channel_identifier="YOUR_CHANNEL_NAME"),
          ],
          action=RuleAction(
              action_type=RuleActionType.ANNOTATION,
              annotation_type=RuleAnnotationType.DATA_REVIEW,
          ),
      )
  )

  print(rule)
  ```
</CodeGroup>

<Info>
  **annotationType**:

  * `ANNOTATION_TYPE_DATA_REVIEW` creates an issue-tracking Annotation with a [status](/documentation/reference/annotations-reference#statuses) workflow, the right choice for most Rule actions like this one. It can be assigned to a user, and starts as **Open** by default until someone marks it **Failed** or **Accepted** during review.
  * `ANNOTATION_TYPE_PHASE` creates an informational milestone marker with no status instead, and won't behave the same way for a condition like this.
</Info>

## Preview an Ad Hoc Rule

Call the [`EvaluateRulesPreview`](/api-reference/ruleevaluationservice/evaluaterulespreview) endpoint to see what Annotations it would generate, without creating a Report or saving any Annotations. Use the Rule ID from the previous step.

```bash curl theme={null}
curl -X POST -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" $SIFT_REST_URL/api/v1/rules/evaluate-rules:preview -d '{
  "run": {
    "id": "RUN_ID"
  },
  "rules": {
    "rules": { "ids": { "ids": ["RULE_ID"] } }
  }
}'
```

<Info>
  **Ingestion ordering**: If you create a Run, stream Channel data to it, and immediately call `EvaluateRulesPreview`, the preview can run before the streamed data finishes processing, causing a "no matching Channels found for Rule" error. To guarantee the Channels are available before evaluation, use a [data import](/documentation/ingest/data-import/import-data-from-a-file) instead of streaming. The Channels are available once the import completes.
</Info>

<Info>
  **Python**: Unlike the Create and Evaluate steps, the official Python client doesn't yet have a method to preview an Ad Hoc Rule evaluation. Support is coming soon.
</Info>

## Evaluate an Ad Hoc Rule

Call the [`EvaluateRules`](/api-reference/ruleevaluationservice/evaluaterules) endpoint with a Run ID and the Rule ID from the previous step.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" $SIFT_REST_URL/api/v1/rules/evaluate-rules -d '{
    "run": {
      "id": "RUN_ID"
    },
    "rules": {
      "rules": { "ids": { "ids": ["RULE_ID"] } }
    },
    "reportName": "REPORT_NAME"
  }'
  ```

  ```python Python theme={null}
  # To run these examples, install the client: pip install sift-stack-py
  # These examples are written for sift-stack-py v0.19.1
  import os
  from dotenv import load_dotenv
  from sift_client import SiftClient

  load_dotenv()

  client = SiftClient(
      api_key=os.getenv("SIFT_API_KEY"),
      grpc_url=os.getenv("SIFT_GRPC_URI"),
      rest_url=os.getenv("SIFT_REST_URI"),
  )

  job = client.reports.create_from_rules(
      name="REPORT_NAME",
      run="RUN_ID",
      rules=["RULE_ID"],
  )

  report = client.reports.wait_until_complete(job=job)

  print(report)
  ```
</CodeGroup>

<Info>
  **Asynchronous processing**: It's common for `EvaluateRules` to return a `jobId`, even when `reportId` is already set. This means the evaluation is still processing and `createdAnnotationCount` in this response isn't final yet. Poll [`ListJobs`](/api-reference/jobservice/listjobs) with a `job_id` filter until the job completes to get the actual count.
</Info>

## Verify

<MintTable
  columns={['Endpoint', 'Check', 'Notes']}
  columnWidths={['25%', '35%', '40%']}
  rows={[
['[BatchUpdateRules](/api-reference/ruleservice/batchupdaterules)', 'In the response of `BatchUpdateRules`, check `createdRuleIdentifiers` to confirm the Ad Hoc Rule was created and get its ID.', ''],
['[EvaluateRulesPreview](/api-reference/ruleevaluationservice/evaluaterulespreview)', 'In the response of `EvaluateRulesPreview`, check `createdAnnotationCount` and `dryRunAnnotations` to confirm what the evaluation would create before running it for real.', ''],
['[EvaluateRules](/api-reference/ruleevaluationservice/evaluaterules)', 'In the response of `EvaluateRules`, check `createdAnnotationCount` to confirm how many Annotations were actually created.', 'If `jobId` is set, poll [ListJobs](/api-reference/jobservice/listjobs) with a `job_id` filter until the job completes to get the final count, even if `reportId` is already present.'],
]}
/>
