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:
{
"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
Use the Get Dynamic Rules Schema endpoint to discover all supported fields, available context variables, functions, and operators for your organization.
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.
The exact set of available variables depends on your organization’s configuration. Use the schema endpoint to discover what’s available.
Workflow
Step 1: Explore the Schema
Discover available fields, variables, operators, and functions:
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 →
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:
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 →
You can also pass a sample_context to test evaluation:
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:
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:
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 →
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:
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": [
{"trade_date": "2025-06-15"},
{"trade_date": "2025-12-15"}
],
"include_baseline": true
}'
View API Reference →
Examples
Year-End Tax-Loss Harvesting
Increase the tax-loss harvesting aggressiveness during December:
{
"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:
{
"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:
{
"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.