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

# Dynamic Rules Guide

> Automatically adjust optimization parameters based on time schedules and conditions.

## Overview

Dynamic rules let you make optimization config values change automatically based on **time** (cron schedules) and **conditions** (expressions evaluated against portfolio context). Instead of manually updating configs when market conditions change or calendar events occur, you define rules that activate automatically.

Common use cases:

* Increase tax-loss harvesting aggressiveness in December
* Adjust position weight limits based on AUM
* Change optimization mode based on portfolio characteristics
* Apply different parameters for accounts meeting specific criteria

## Rule Structure

Dynamic rules are attached to a config (organization, portfolio, or account level) via the `dynamic_rules` array. Each entry targets a specific config field and contains one or more rules:

```json theme={null}
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "cron": "",
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 0
        },
        {
          "cron": "",
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 1
        }
      ]
    }
  ]
}
```

### Fields

| Field        | Type   | Required | Description                                                                      |
| ------------ | ------ | -------- | -------------------------------------------------------------------------------- |
| `field_name` | string | Yes      | Config field to set dynamically (e.g. `max_weight`, `tax_preferences.tax_gamma`) |
| `rules`      | array  | Yes      | One or more rule entries                                                         |

### Rule Entry Fields

| Field       | Type    | Required | Default | Description                                                                           |
| ----------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------- |
| `cron`      | string  | No       | `""`    | Cron expression for time-based activation (e.g. `* * * 12 *` for December)            |
| `condition` | string  | No       | `""`    | Boolean expression evaluated against context variables (e.g. `aum > 1000000`)         |
| `value`     | any     | Yes      | —       | Value to apply when the rule matches                                                  |
| `priority`  | integer | No       | `0`     | Evaluation order (0-9999). Lowest number is evaluated first; first matching rule wins |

A rule matches when **both** its cron schedule and condition are satisfied. An empty cron or condition field is treated as always matching, but at least one of the two must be provided.

## Rule Resolution

For each dynamic rule field (e.g. `max_weight`), the system follows a three-step resolution path:

### Step 1: Collect rules from all config levels

Rules from every config level are gathered and given a priority offset so that higher-level configs always take precedence:

| Config level    | Priority offset   | Precedence |
| --------------- | ----------------- | ---------- |
| Account rules   | priority + 0      | Highest    |
| Portfolio rules | priority + 10,000 | Middle     |
| Org rules       | priority + 20,000 | Lowest     |

### Step 2: Find first static value and prune

Walk the config chain from most specific to least. The first level that sets a static value for the field becomes the default. Rules from levels **below** that level are pruned — rules at the same level still apply.

```
Account config has field?
 |
 |-- YES --> default = account value
 |           prune rules with priority >= 10,000
 |           (portfolio + org rules removed; account rules survive)
 |
 NO
 |
 v
Portfolio config has field?
 |
 |-- YES --> default = portfolio value
 |           prune rules with priority >= 20,000
 |           (org rules removed; account + portfolio rules survive)
 |
 NO
 |
 v
Org config has field?
 |
 |-- YES --> default = org value
 |           no pruning needed (nothing below org)
 |           (all rules survive)
 |
 NO
 |
 v
default = None (no static value anywhere, no rules pruned)
```

### Step 3: Evaluate remaining rules

Remaining rules are sorted by priority ascending. **First matching rule wins** — the first rule whose cron and condition both evaluate to true is used. If no rule matches, the default value from step 2 is used.

**Key concepts:**

* **Priority offsets**: Account rules keep their raw priority, portfolio rules add 10,000, and org rules add 20,000. This means account-level rules are always evaluated before lower levels.
* **Static value pruning**: When a static value is found at a config level, rules from levels below are pruned. Rules at the same level as the static value still apply. This prevents lower-level dynamic rules from overriding an explicit static value set at a higher level.
* **First match wins**: Rules are evaluated in ascending priority order. The first rule whose cron and condition both match is used.
* **Fallback**: If no rule matches, the static default value from step 2 is used.

## Supported Fields

### Config Fields

Dynamic rules can target any config field, including:

* `max_weight`, `min_weight` — position weight limits
* `optimization_mode` — optimization strategy
* `tax_gamma` — tax-loss harvesting aggressiveness
* `cash_buffer`, `cash_buffer_usd`, `cash_buffer_cad` — cash reserves
* `round_lot_size` — trade rounding

### Tax Preference Fields

Use the `tax_preferences.` prefix for tax preference fields:

* `tax_preferences.tax_gamma`
* `tax_preferences.short_term_rate`
* `tax_preferences.long_term_rate`

<Tip>
  Use the [Get Dynamic Rules Schema](/api-reference/endpoint/get-dynamic-rules-schema) endpoint to discover all supported fields, available context variables, functions, and operators for your organization.
</Tip>

## Context Variables

Conditions in dynamic rules are evaluated against context variables that describe the portfolio and account at optimization time.

### Portfolio-Level Variables

Variables like `aum`, `cash_balance`, and `position_count` describe the current state of the portfolio being optimized.

### Account-Level Variables

Variables like `organization_name` and `client_external_id` describe the account and organization.

### Fundamental Per-Ticker Variables

Variables like `market_cap` and `pe_ratio` provide fundamental data for individual securities. These are available when rules are evaluated at the ticker level.

### Enrichment Columns

Custom enrichment data columns uploaded to your organization are also available as context variables.

<Info>
  The exact set of available variables depends on your organization's configuration. Use the schema endpoint to discover what's available.
</Info>

## Workflow

### Step 1: Explore the Schema

Discover available fields, variables, operators, and functions:

```bash theme={null}
curl --request GET \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/schema/ \
  --header 'X-Api-Key: <api-key>'
```

[View API Reference →](/api-reference/endpoint/get-dynamic-rules-schema)

The response includes:

* **fields**: Config fields that support dynamic rules
* **variables**: Context variables available in conditions
* **functions**: Functions you can use in expressions (e.g. `abs()`, `max()`)
* **operators**: Supported operators (`>`, `<`, `==`, `and`, `or`, etc.)
* **examples**: Example expressions

### Step 2: Validate Expressions

Before deploying rules, validate that your condition expressions are syntactically correct:

```bash theme={null}
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/validate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "expression": "aum > 1000000 and position_count < 50"
  }'
```

[View API Reference →](/api-reference/endpoint/post-dynamic-rules-validate)

You can also pass a `sample_context` to test evaluation:

```bash theme={null}
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/validate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "expression": "aum > 1000000",
    "sample_context": {"aum": 2000000}
  }'
```

### Step 3: Attach Rules to a Config

Add dynamic rules when creating or updating a config at any level. For example, to add rules to a portfolio config:

```bash theme={null}
curl --request PATCH \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/portfolio_configs/{portfolio_config_uuid}/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "customization_config": {
      "dynamic_rules": [
        {
          "field_name": "max_weight",
          "rules": [
            {
              "condition": "aum > 1000000",
              "value": 0.08,
              "priority": 0
            }
          ]
        }
      ]
    }
  }'
```

### Step 4: Simulate Rule Evaluation

Test how rules resolve for a specific account and date without running an optimization:

```bash theme={null}
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/simulate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_account_uuid": "account-uuid-here",
    "trade_date": "2025-12-15"
  }'
```

[View API Reference →](/api-reference/endpoint/post-dynamic-rules-simulate)

The response shows:

* **resolved\_values**: The final config values after rule evaluation
* **context**: The context variables used during evaluation
* **rule\_evaluations**: Which rules matched and which didn't, with details

You can also pass `dynamic_rules` directly to simulate rules that haven't been saved yet.

### Step 5: Preview Optimization Impact

See how dynamic rules affect actual optimization results by comparing scenarios. Preview runs asynchronously — you submit a job, then poll for results.

#### Submit a preview job

POST your scenarios to create a preview job. Each scenario requires a `label` and `context_overrides` (e.g. to vary `aum` or `trade_date`). You can optionally include `as_of_datetime`, `fundamentals_overrides`, and `config_overrides`.

```bash theme={null}
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/preview/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_account_uuid": "account-uuid-here",
    "scenarios": [
      {
        "label": "Summer baseline",
        "context_overrides": {"trade_date": "2025-06-15"}
      },
      {
        "label": "Year-end TLH",
        "context_overrides": {"trade_date": "2025-12-15"}
      }
    ],
    "include_baseline": true
  }'
```

The endpoint returns `202 Accepted` with a job object:

```json theme={null}
{
  "id": "job-uuid",
  "status": "PENDING",
  "params": { ... },
  "result": null,
  "error_message": null,
  "created_at": "2025-06-01T12:00:00Z",
  "started_at": null,
  "finished_at": null
}
```

[View API Reference →](/api-reference/endpoint/post-dynamic-rules-preview)

#### Poll for results

Use the job ID to check status and retrieve results:

```bash theme={null}
curl --request GET \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/preview/{job_id}/ \
  --header 'X-Api-Key: <api-key>'
```

| Status    | Meaning                                        |
| --------- | ---------------------------------------------- |
| `PENDING` | Job is queued                                  |
| `RUNNING` | Optimization is in progress                    |
| `SUCCESS` | Results are available in the `result` field    |
| `FAILURE` | Job failed — check `error_message` for details |

<Tip>
  Poll every 2–5 seconds. Most preview jobs complete within 30 seconds.
</Tip>

[View API Reference →](/api-reference/endpoint/get-dynamic-rules-preview-job)

#### List previous jobs

Browse your organization's preview job history:

```bash theme={null}
curl --request GET \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/preview/jobs/ \
  --header 'X-Api-Key: <api-key>'
```

[View API Reference →](/api-reference/endpoint/list-dynamic-rules-preview-jobs)

## Examples

### Year-End Tax-Loss Harvesting

Increase the tax-loss harvesting aggressiveness during December:

```json theme={null}
{
  "dynamic_rules": [
    {
      "field_name": "tax_preferences.tax_gamma",
      "rules": [
        {
          "cron": "* * * 12 *",
          "value": 5.0,
          "priority": 0
        }
      ]
    }
  ]
}
```

**Resolution in December (e.g. trade date 2025-12-15):**

```
Rule priority 0: cron "* * * 12 *" → matches → value = 5.0 ✓ MATCH
                                                ────────
                                      Result:   tax_gamma = 5.0
```

**Resolution outside December (e.g. trade date 2025-06-15):**

```
Rule priority 0: cron "* * * 12 *" → does not match → skipped
                 No rules matched → use default from static config
```

### AUM-Based Weight Limits

Tighten position concentration limits for larger portfolios:

```json theme={null}
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 0
        },
        {
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 1
        }
      ]
    }
  ]
}
```

**Resolution for a portfolio with 2M AUM:**

```
Rule priority 0: condition "aum > 1000000" → true  → value = 0.08 ✓ MATCH
                                                     ────────
                                           Result:   max_weight = 0.08
```

**Resolution for a portfolio with 10M AUM:**

```
Rule priority 0: condition "aum > 1000000" → true → value = 0.08 ✓ MATCH
                                                    ────────
                                          Result:   max_weight = 0.08
```

Both portfolios match the first rule. If you want different behavior for larger portfolios, put the more specific condition at a lower priority number so it matches first:

```json theme={null}
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 0
        },
        {
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 1
        }
      ]
    }
  ]
}
```

Now a 10M AUM portfolio matches priority 0 first and gets `0.05`, while a 2M AUM portfolio skips priority 0 and matches priority 1 for `0.08`.

### Multi-Level Rules with Static Value Pruning

This example shows how rules accumulate across config levels and how a static value prunes lower-level rules.

**Config state for `max_weight`:**

```
Account:    rule {cron: Dec → 0.50, priority: 1}   max_weight = NULL
Portfolio:  rule {always → 0.06, priority: 1}       max_weight = 0.05
Org:        rule {always → 0.03, priority: 1}       max_weight = NULL
```

**Step 1 — Collect and offset rules:**

```
Account rule:   {cron: Dec, value: 0.50, priority: 1 + 0     = 1}
Portfolio rule: {always,     value: 0.06, priority: 1 + 10000 = 10001}
Org rule:       {always,     value: 0.03, priority: 1 + 20000 = 20001}
```

**Step 2 — Find static value and prune:**

```
Account config  → max_weight = NULL → skip
Portfolio config → max_weight = 0.05 → default = 0.05
                   Prune rules with priority ≥ 10,000
                   ├── Portfolio rule (pri 10001) → PRUNED
                   └── Org rule       (pri 20001) → PRUNED

Remaining rules: [{cron: Dec, value: 0.50, priority: 1}]
Default value:   0.05
```

**Step 3 — Evaluate remaining rules:**

**In December:**

```
Rule priority 1: cron "Dec" → matches → value = 0.50 ✓ MATCH
                                         ────────
                               Result:   max_weight = 0.50
```

**In June:**

```
Rule priority 1: cron "Dec" → does not match → skipped
                  No rules matched → use default
                                     ────────
                           Result:   max_weight = 0.05 (portfolio static)
```

The portfolio's static `max_weight = 0.05` serves two purposes: it becomes the fallback default, and it prunes both the portfolio-level and org-level dynamic rules. Only the account-level rule (which has higher precedence) survives.

## Best Practices

* **Simulate before deploying** — Always use the simulate endpoint to verify rule behavior before saving rules to a config. Check that the right rules match and produce expected values.
* **Start priorities at 0** — Use sequential integers starting from 0 (0, 1, 2, ...). You can leave gaps if you anticipate inserting rules later.
* **Understand static blocking** — Be intentional about static values at higher config levels, as they suppress dynamic rules from lower levels for that field.
* **Use the schema endpoint** — Discover available fields and variables programmatically rather than hardcoding. The available set may expand over time.
* **Keep conditions simple** — Prefer clear, readable expressions. Use the validate endpoint to catch syntax errors early.
