this is all of the changes now

This commit is contained in:
2026-02-18 18:43:42 -06:00
parent 77cf18c02f
commit a1b9b8d2b1
22 changed files with 619 additions and 535 deletions

View File

@@ -32,8 +32,8 @@ Rules are the core automation logic in Attune that connect triggers to actions.
},
"action_params": {
"channel": "#alerts",
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
"severity": "{{ trigger.payload.severity }}"
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
"severity": "{{ event.payload.severity }}"
},
"enabled": true,
"created": "2024-01-13T10:00:00Z",
@@ -64,7 +64,7 @@ Rules are the core automation logic in Attune that connect triggers to actions.
The `action_params` field supports both static values and dynamic templates:
- **Static values**: `"channel": "#alerts"`
- **Dynamic from trigger payload**: `"message": "{{ trigger.payload.message }}"`
- **Dynamic from event payload**: `"message": "{{ event.payload.message }}"`
- **Dynamic from pack config**: `"token": "{{ pack.config.api_token }}"`
- **System variables**: `"timestamp": "{{ system.timestamp }}"`
@@ -295,8 +295,8 @@ Create a new rule in the system.
},
"action_params": {
"channel": "#alerts",
"message": "Error detected: {{ trigger.payload.message }}",
"severity": "{{ trigger.payload.severity }}"
"message": "Error detected: {{ event.payload.message }}",
"severity": "{{ event.payload.severity }}"
},
"enabled": true
}
@@ -314,7 +314,7 @@ Create a new rule in the system.
- `conditions`: JSON Logic conditions for rule evaluation (default: `{}`)
- `action_params`: Parameters to pass to the action (default: `{}`)
- Supports static values: `"channel": "#alerts"`
- Supports dynamic templates: `"message": "{{ trigger.payload.message }}"`
- Supports dynamic templates: `"message": "{{ event.payload.message }}"`
- Supports pack config: `"token": "{{ pack.config.api_token }}"`
- `enabled`: Whether the rule is active (default: `true`)
@@ -341,8 +341,8 @@ Create a new rule in the system.
},
"action_params": {
"channel": "#alerts",
"message": "Error detected: {{ trigger.payload.message }}",
"severity": "{{ trigger.payload.severity }}"
"message": "Error detected: {{ event.payload.message }}",
"severity": "{{ event.payload.severity }}"
},
"enabled": true,
"created": "2024-01-13T10:00:00Z",
@@ -384,7 +384,7 @@ All fields are optional. Only provided fields will be updated.
},
"action_params": {
"channel": "#critical-alerts",
"message": "CRITICAL: {{ trigger.payload.service }} - {{ trigger.payload.message }}",
"message": "CRITICAL: {{ event.payload.service }} - {{ event.payload.message }}",
"priority": "high"
},
"enabled": false
@@ -416,7 +416,7 @@ All fields are optional. Only provided fields will be updated.
},
"action_params": {
"channel": "#critical-alerts",
"message": "CRITICAL: {{ trigger.payload.service }} - {{ trigger.payload.message }}",
"message": "CRITICAL: {{ event.payload.service }} - {{ event.payload.message }}",
"priority": "high"
},
"enabled": false,

View File

@@ -177,7 +177,7 @@ attune rule create \
--pack core \
--trigger core.webhook \
--action core.notify \
--criteria '{"trigger.payload.severity": "critical"}'
--criteria '{"event.payload.severity": "critical"}'
```
#### Delete Rule

View File

@@ -35,7 +35,7 @@ This document provides practical, copy-paste ready examples of rule parameter ma
---
## Example 2: Dynamic from Trigger Payload
## Example 2: Dynamic from Event Payload
**Use Case:** Alert with error details from event
@@ -58,9 +58,9 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"action_ref": "slack.post_message",
"action_params": {
"channel": "#incidents",
"message": "🚨 Error in {{ trigger.payload.service }}: {{ trigger.payload.error }}",
"severity": "{{ trigger.payload.severity }}",
"timestamp": "{{ trigger.payload.timestamp }}"
"message": "🚨 Error in {{ event.payload.service }}: {{ event.payload.error }}",
"severity": "{{ event.payload.severity }}",
"timestamp": "{{ event.payload.timestamp }}"
},
"enabled": true
}
@@ -163,8 +163,8 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"action_params": {
"token": "{{ pack.config.token }}",
"repo": "{{ pack.config.repo_owner }}/{{ pack.config.repo_name }}",
"title": "[{{ trigger.payload.severity }}] {{ trigger.payload.service }}: {{ trigger.payload.error_message }}",
"body": "Error Details:\n\nService: {{ trigger.payload.service }}\nSeverity: {{ trigger.payload.severity }}\n\nStack Trace:\n{{ trigger.payload.stack_trace }}",
"title": "[{{ event.payload.severity }}] {{ event.payload.service }}: {{ event.payload.error_message }}",
"body": "Error Details:\n\nService: {{ event.payload.service }}\nSeverity: {{ event.payload.severity }}\n\nStack Trace:\n{{ event.payload.stack_trace }}",
"labels": ["bug", "automated"],
"assignees": ["oncall"]
},
@@ -219,12 +219,12 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"trigger_ref": "core.user_event",
"action_ref": "audit.log",
"action_params": {
"user_id": "{{ trigger.payload.user.id }}",
"user_name": "{{ trigger.payload.user.profile.name }}",
"user_email": "{{ trigger.payload.user.profile.email }}",
"department": "{{ trigger.payload.user.profile.department }}",
"action": "{{ trigger.payload.action }}",
"ip_address": "{{ trigger.payload.metadata.ip }}"
"user_id": "{{ event.payload.user.id }}",
"user_name": "{{ event.payload.user.profile.name }}",
"user_email": "{{ event.payload.user.profile.email }}",
"department": "{{ event.payload.user.profile.department }}",
"action": "{{ event.payload.action }}",
"ip_address": "{{ event.payload.metadata.ip }}"
},
"enabled": true
}
@@ -271,10 +271,10 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"action_ref": "slack.post_message",
"action_params": {
"channel": "#alerts",
"message": "Primary error: {{ trigger.payload.errors.0 }}",
"secondary_error": "{{ trigger.payload.errors.1 }}",
"environment": "{{ trigger.payload.tags.0 }}",
"severity": "{{ trigger.payload.tags.1 }}"
"message": "Primary error: {{ event.payload.errors.0 }}",
"secondary_error": "{{ event.payload.errors.1 }}",
"environment": "{{ event.payload.tags.0 }}",
"severity": "{{ event.payload.tags.1 }}"
},
"enabled": true
}
@@ -377,17 +377,17 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"routing_key": "{{ pack.config.routing_key }}",
"event_action": "trigger",
"payload": {
"summary": "{{ trigger.payload.metric_name }} exceeded threshold on {{ trigger.payload.host }}",
"summary": "{{ event.payload.metric_name }} exceeded threshold on {{ event.payload.host }}",
"severity": "critical",
"source": "{{ trigger.payload.host }}",
"source": "{{ event.payload.host }}",
"custom_details": {
"metric": "{{ trigger.payload.metric_name }}",
"current_value": "{{ trigger.payload.current_value }}",
"threshold": "{{ trigger.payload.threshold }}",
"duration": "{{ trigger.payload.duration_seconds }}s"
"metric": "{{ event.payload.metric_name }}",
"current_value": "{{ event.payload.current_value }}",
"threshold": "{{ event.payload.threshold }}",
"duration": "{{ event.payload.duration_seconds }}s"
}
},
"dedup_key": "{{ trigger.payload.host }}_{{ trigger.payload.metric_name }}"
"dedup_key": "{{ event.payload.host }}_{{ event.payload.metric_name }}"
},
"enabled": true
}
@@ -463,27 +463,27 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"fields": [
{
"title": "Service",
"value": "{{ trigger.payload.service }}",
"value": "{{ event.payload.service }}",
"short": true
},
{
"title": "Version",
"value": "{{ trigger.payload.version }}",
"value": "{{ event.payload.version }}",
"short": true
},
{
"title": "Environment",
"value": "{{ trigger.payload.environment }}",
"value": "{{ event.payload.environment }}",
"short": true
},
{
"title": "Deployed By",
"value": "{{ trigger.payload.deployed_by }}",
"value": "{{ event.payload.deployed_by }}",
"short": true
}
],
"footer": "Attune Automation",
"ts": "{{ trigger.payload.timestamp }}"
"ts": "{{ event.payload.timestamp }}"
}
]
},
@@ -515,9 +515,9 @@ This document provides practical, copy-paste ready examples of rule parameter ma
"trigger_ref": "core.alert_event",
"action_ref": "slack.post_message",
"action_params": {
"channel": "{{ trigger.payload.severity | default: 'info' | map: {'critical': '#incidents', 'high': '#alerts', 'medium': '#monitoring', 'low': '#logs'} }}",
"message": "{{ trigger.payload.message }}",
"color": "{{ trigger.payload.severity | map: {'critical': 'danger', 'high': 'warning', 'medium': 'good', 'low': '#cccccc'} }}"
"channel": "{{ event.payload.severity | default: 'info' | map: {'critical': '#incidents', 'high': '#alerts', 'medium': '#monitoring', 'low': '#logs'} }}",
"message": "{{ event.payload.message }}",
"color": "{{ event.payload.severity | map: {'critical': 'danger', 'high': 'warning', 'medium': 'good', 'low': '#cccccc'} }}"
},
"enabled": true
}
@@ -584,12 +584,12 @@ The `config` field should contain the same resolved parameters.
```json
{
"action_params": {
"summary": "Error: {{ trigger.payload.message }}",
"summary": "Error: {{ event.payload.message }}",
"details": {
"service": "{{ trigger.payload.service }}",
"host": "{{ trigger.payload.host }}",
"timestamp": "{{ trigger.payload.timestamp }}",
"stack_trace": "{{ trigger.payload.stack_trace }}"
"service": "{{ event.payload.service }}",
"host": "{{ event.payload.host }}",
"timestamp": "{{ event.payload.timestamp }}",
"stack_trace": "{{ event.payload.stack_trace }}"
}
}
}
@@ -600,11 +600,11 @@ The `config` field should contain the same resolved parameters.
{
"action_params": {
"user": {
"id": "{{ trigger.payload.user.id }}",
"name": "{{ trigger.payload.user.name }}",
"email": "{{ trigger.payload.user.email }}"
"id": "{{ event.payload.user.id }}",
"name": "{{ event.payload.user.name }}",
"email": "{{ event.payload.user.email }}"
},
"action": "{{ trigger.payload.action_type }}"
"action": "{{ event.payload.action_type }}"
}
}
```

View File

@@ -226,7 +226,7 @@ Update the rule to use event data:
```sql
UPDATE attune.rule
SET action_params = '{"message": "Timer fired at {{ trigger.payload.fired_at }}"}'::jsonb
SET action_params = '{"message": "Timer fired at {{ event.payload.fired_at }}"}'::jsonb
WHERE ref = 'core.rule.timer_10s_echo';
```

View File

@@ -220,7 +220,7 @@ Potential improvements for the parameter form system:
1. **Advanced validation**: Support for min/max, pattern matching, custom validators
2. **Conditional fields**: Show/hide fields based on other field values
3. **Field hints**: Helper text, examples, tooltips
4. **Template variables**: Autocomplete for Jinja2 template syntax (e.g., `{{ trigger.payload.* }}`)
4. **Template variables**: Autocomplete for Jinja2 template syntax (e.g., `{{ event.payload.* }}`)
5. **Schema versioning**: Handle schema changes across pack versions
6. **Array item editing**: Better UX for editing array items individually
7. **Nested objects**: Support for deeply nested object schemas

View File

@@ -2,22 +2,20 @@
## Quick Reference
This document provides a quick overview of what exists and what needs to be implemented for rule parameter mapping.
This document tracks the implementation status of rule parameter mapping — the system that resolves `{{ }}` template variables in rule `action_params` before passing them to action executions.
---
## ✅ What Already Exists
## ✅ Completed
### Database Schema
- **Migration:** `migrations/20240103000003_add_rule_action_params.sql`
- **Column:** `attune.rule.action_params` (JSONB, default `{}`)
- **Column:** `rule.action_params` (JSONB, default `{}`)
- **Index:** `idx_rule_action_params_gin` (GIN index for efficient querying)
- **Status:** ✅ Complete
### Data Models
- **File:** `crates/common/src/models.rs`
- **Struct:** `rule::Rule` has `pub action_params: JsonValue` field
- **Status:** ✅ Complete
### API Layer
- **File:** `crates/api/src/dto/rule.rs`
@@ -26,232 +24,115 @@ This document provides a quick overview of what exists and what needs to be impl
- `UpdateRuleRequest.action_params` (optional)
- **Response DTOs:**
- `RuleResponse.action_params`
- **Status:** ✅ Complete
- `RuleSummary.action_params`
### Repository Layer
- **File:** `crates/common/src/repositories/rule.rs`
- **Operations:**
- `CreateRuleInput.action_params` included in INSERT
- `UpdateRuleInput.action_params` handled in UPDATE
- All SELECT queries include `action_params` column
- **Status:** ✅ Complete
- **Operations:** CREATE, UPDATE, and SELECT all handle `action_params`
### API Routes
- **File:** `crates/api/src/routes/rules.rs`
- **Handlers:**
- `create_rule()` accepts `action_params` from request
- `update_rule()` updates `action_params` if provided
- **Status:** ✅ Complete
### Template Resolver Module
- **File:** `crates/common/src/template_resolver.rs`
- **Struct:** `TemplateContext` with `event`, `pack_config`, and `system_vars` fields
- **Function:** `resolve_templates()` — recursively resolves `{{ }}` templates in JSON values
- **Re-exported** from `attune_common::template_resolver` and `attune_common::{TemplateContext, resolve_templates}`
- **Also re-exported** from `attune_sensor::template_resolver` for backward compatibility
- **20 unit tests** covering all template features
### Data Flow (Static Parameters)
### Template Syntax
**Available Sources:**
| Namespace | Example | Description |
|-----------|---------|-------------|
| `event.payload.*` | `{{ event.payload.service }}` | Event payload data |
| `event.id` | `{{ event.id }}` | Event database ID |
| `event.trigger` | `{{ event.trigger }}` | Trigger ref that generated the event |
| `event.created` | `{{ event.created }}` | Event creation timestamp (RFC 3339) |
| `pack.config.*` | `{{ pack.config.api_token }}` | Pack configuration values |
| `system.*` | `{{ system.timestamp }}` | System-provided variables |
### Integration in Executor
- **File:** `crates/executor/src/event_processor.rs`
- **Method:** `resolve_action_params()` builds a `TemplateContext` from the event and rule, then calls `resolve_templates()`
- **Context includes:**
- `event.id`, `event.trigger`, `event.created`, `event.payload.*` from the `Event` model
- `system.timestamp` (current time), `system.rule.id`, `system.rule.ref`
- **Called during:** enforcement creation in `create_enforcement()`
### Data Flow
```
Rule.action_params (static JSON)
Enforcement.config (copied verbatim)
Rule.action_params (templates)
resolve_templates() in EventProcessor
Enforcement.config (resolved values)
Execution.config (passed through)
Worker (receives as action parameters)
```
- **Status:** ✅ Working for static values
### Template Features
- ✅ Static values pass through unchanged
- ✅ Single-template type preservation (numbers, booleans, objects, arrays)
- ✅ String interpolation with multiple templates
- ✅ Nested object access via dot notation (`event.payload.metadata.host`)
- ✅ Array element access by index (`event.payload.tags.0`)
- ✅ Missing values resolve to `null` with warning logged
- ✅ Empty/null action_params handled gracefully
### Documentation
-`docs/workflows/rule-parameter-mapping.md` — comprehensive user guide
-`docs/examples/rule-parameter-examples.md` — real-world examples
-`docs/api/api-rules.md` — API documentation
- ✅ Inline code documentation in `template_resolver.rs`
---
## ❌ What's Missing
### Template Resolution Logic
- **Needed:** Parse and resolve `{{ }}` templates in `action_params`
- **Location:** `crates/sensor/src/` (new module needed)
- **Status:** ❌ Not implemented
### Template Resolver Module
```rust
// NEW FILE: crates/sensor/src/template_resolver.rs
pub struct TemplateContext {
pub trigger_payload: JsonValue,
pub pack_config: JsonValue,
pub system_vars: JsonValue,
}
pub fn resolve_templates(
params: &JsonValue,
context: &TemplateContext
) -> Result<JsonValue> {
// Implementation needed
}
```
- **Status:** ❌ Does not exist
## 🔄 Partially Implemented
### Pack Config Loading
- **Needed:** Load pack configuration from database
- **Current:** Rule matcher doesn't load pack config
- **Required for:** `{{ pack.config.* }}` templates
- **Status:** ❌ Not implemented
### Integration in Rule Matcher
- **File:** `crates/sensor/src/rule_matcher.rs`
- **Method:** `create_enforcement()`
- **Current code (line 309):**
```rust
let config = Some(&rule.action_params);
```
- **Needed code:**
```rust
// Load pack config
let pack_config = self.load_pack_config(&rule.pack_ref).await?;
// Build template context
let context = TemplateContext {
trigger_payload: event.payload.clone().unwrap_or_default(),
pack_config,
system_vars: self.build_system_vars(rule, event),
};
// Resolve templates
let resolved_params = resolve_templates(&rule.action_params, &context)?;
let config = Some(resolved_params);
```
- **Status:** ❌ Not implemented
### Unit Tests
- **File:** `crates/sensor/src/template_resolver.rs` (tests module)
- **Needed tests:**
- Simple string substitution
- Nested object access
- Array element access
- Type preservation
- Missing value handling
- Pack config reference
- System variables
- Multiple templates in one string
- Invalid syntax handling
- **Status:** ❌ Not implemented
### Integration Tests
- **Needed:** End-to-end test of template resolution
- **Scenario:** Create rule with templates → fire event → verify enforcement has resolved params
- **Status:** ❌ Not implemented
- **Current:** Executor passes empty `{}` for `pack.config` context
- **Needed:** Load pack configuration from database before template resolution
- **Impact:** `{{ pack.config.* }}` templates resolve to `null` until implemented
- **TODO comment** in `event_processor.rs` marks the location
---
## 📋 Implementation Checklist
## 📋 Remaining Work
### Phase 1: MVP (2-3 days)
### Phase 1: Complete Core (Short-term)
- [ ] **Create template resolver module**
- [ ] Define `TemplateContext` struct
- [ ] Implement `resolve_templates()` function
- [ ] Regex pattern matching for `{{ }}`
- [ ] JSON path extraction with dot notation
- [ ] Type preservation logic
- [ ] Error handling for missing values
- [ ] Unit tests (9+ test cases)
- [ ] **Pack config loading** — Load pack config from database for `{{ pack.config.* }}` resolution
- [ ] **Integration tests** — End-to-end test: create rule with templates → fire event → verify enforcement has resolved params
- [ ] **Add pack config loading**
- [ ] Add method to load pack config from database
- [ ] Implement in-memory cache with TTL
- [ ] Handle missing pack config gracefully
### Phase 2: Advanced Features (Future)
- [ ] **Integrate with rule matcher**
- [ ] Update `create_enforcement()` method
- [ ] Load pack config before resolution
- [ ] Build template context
- [ ] Call template resolver
- [ ] Handle resolution errors
- [ ] Log warnings for missing values
- [ ] **System variables**
- [ ] Build system context (timestamp, rule ID, event ID)
- [ ] Document available system variables
- [ ] **Testing**
- [ ] Unit tests for template resolver
- [ ] Integration test: end-to-end flow
- [ ] Test with missing values
- [ ] Test with nested objects
- [ ] Test with arrays
- [ ] Test performance (benchmark)
- [ ] **Documentation**
- [x] User documentation (`docs/rule-parameter-mapping.md`) ✅
- [x] API documentation updates (`docs/api-rules.md`) ✅
- [ ] Code documentation (inline comments)
- [ ] Update sensor service docs
### Phase 2: Advanced Features (1-2 days, future)
- [ ] **Default values**
- [ ] Parse `| default: 'value'` syntax
- [ ] Apply defaults when value is null/missing
- [ ] Unit tests
- [ ] **Filters**
- [ ] `upper` - Convert to uppercase
- [ ] `lower` - Convert to lowercase
- [ ] `trim` - Remove whitespace
- [ ] `date: <format>` - Format timestamp
- [ ] `truncate: <length>` - Truncate string
- [ ] `json` - Serialize to JSON string
- [ ] Unit tests for each filter
- [ ] **Performance optimization**
- [ ] Cache compiled regex patterns
- [ ] Skip resolution if no `{{ }}` found
- [ ] Parallel template resolution
- [ ] Benchmark improvements
- [ ] **Default values** — Parse `| default: 'value'` syntax for fallback values
- [ ] **Filters**`upper`, `lower`, `trim`, `date`, `truncate`, `json`
- [ ] **Conditional templates**`{% if event.payload.severity == 'critical' %}...{% endif %}`
- [ ] **Performance** — Skip resolution early if no `{{ }}` patterns detected in action_params
---
## 🔍 Key Implementation Details
## 🔍 Template Example
### Current Enforcement Creation (line 306-348)
```rust
async fn create_enforcement(&self, rule: &Rule, event: &Event) -> Result<Id> {
let payload = event.payload.clone().unwrap_or_default();
let config = Some(&rule.action_params); // ← This line needs to change
let enforcement_id = sqlx::query_scalar!(
r#"
INSERT INTO attune.enforcement
(rule, rule_ref, trigger_ref, config, event, status, payload, condition, conditions)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
"#,
Some(rule.id),
&rule.r#ref,
&rule.trigger_ref,
config, // ← Resolved params go here
Some(event.id),
EnforcementStatus::Created as EnforcementStatus,
payload,
EnforcementCondition::All as EnforcementCondition,
&rule.conditions
)
.fetch_one(&self.db)
.await?;
// ... rest of method
**Input (Rule `action_params`):**
```json
{
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
"channel": "{{ pack.config.alert_channel }}",
"severity": "{{ event.payload.severity }}",
"event_id": "{{ event.id }}",
"trigger": "{{ event.trigger }}"
}
```
### Template Examples
**Input (Rule):**
**Context (built from Event + Rule):**
```json
{
"action_params": {
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
"channel": "{{ pack.config.alert_channel }}",
"severity": "{{ trigger.payload.severity }}"
}
}
```
**Context:**
```json
{
"trigger": {
"event": {
"id": 456,
"trigger": "core.error_event",
"created": "2026-02-05T10:00:00Z",
"payload": {
"service": "api-gateway",
"message": "Connection timeout",
@@ -262,114 +143,30 @@ async fn create_enforcement(&self, rule: &Rule, event: &Event) -> Result<Id> {
"config": {
"alert_channel": "#incidents"
}
},
"system": {
"timestamp": "2026-02-05T10:00:01Z",
"rule": { "id": 42, "ref": "alerts.error_notification" }
}
}
```
**Output (Enforcement):**
**Output (Enforcement `config`):**
```json
{
"config": {
"message": "Error in api-gateway: Connection timeout",
"channel": "#incidents",
"severity": "critical"
}
"message": "Error in api-gateway: Connection timeout",
"channel": "#incidents",
"severity": "critical",
"event_id": 456,
"trigger": "core.error_event"
}
```
---
## 📊 Dependencies
## Related Documentation
### Existing (Already in Cargo.toml)
- `serde_json` - JSON manipulation ✅
- `regex` - Pattern matching ✅
- `anyhow` - Error handling ✅
- `sqlx` - Database access ✅
### New Dependencies
- **None required** - Can implement with existing dependencies
---
## 🎯 Success Criteria
- [ ] Static parameters continue to work unchanged
- [ ] Can reference `{{ trigger.payload.* }}` fields
- [ ] Can reference `{{ pack.config.* }}` fields
- [ ] Can reference `{{ system.* }}` variables
- [ ] Type preservation (strings, numbers, booleans, objects, arrays)
- [ ] Nested object access with dot notation works
- [ ] Array element access by index works
- [ ] Missing values handled gracefully (null + warning)
- [ ] Invalid syntax handled gracefully (literal + error)
- [ ] Unit tests pass (90%+ coverage)
- [ ] Integration tests pass
- [ ] Documentation accurate and complete
- [ ] No performance regression (<500µs overhead)
- [ ] Backward compatibility maintained (100%)
---
## 🚀 Getting Started
1. **Read documentation:**
- `docs/rule-parameter-mapping.md` - User guide
- `work-summary/2026-01-17-parameter-templating.md` - Technical spec
2. **Review current code:**
- `crates/sensor/src/rule_matcher.rs:306-348` - Where to integrate
- `crates/common/src/models.rs` - Rule model structure
- `migrations/20240103000003_add_rule_action_params.sql` - Schema
3. **Start implementation:**
- Create `crates/sensor/src/template_resolver.rs`
- Write unit tests first (TDD approach)
- Implement template parsing and resolution
- Integrate with rule_matcher
- Run integration tests
4. **Test thoroughly:**
- Unit tests for all edge cases
- Integration test with real database
- Manual testing with example rules
- Performance benchmarks
---
## 📚 Related Documentation
- [Rule Parameter Mapping Guide](./rule-parameter-mapping.md) - Complete user documentation
- [Rule Management API](./api-rules.md) - API reference with examples
- [Sensor Service Architecture](./sensor-service.md) - Service overview
- [Implementation Plan](../work-summary/2026-01-17-parameter-templating.md) - Technical specification
- [Session Summary](../work-summary/2026-01-17-session-parameter-mapping.md) - Discovery notes
---
## 🏷️ Status Summary
| Component | Status | Notes |
|-----------|--------|-------|
| Database schema | ✅ Complete | `action_params` column exists |
| Data models | ✅ Complete | Rule struct has field |
| API DTOs | ✅ Complete | Request/response support |
| API routes | ✅ Complete | CRUD operations work |
| Repository | ✅ Complete | All queries include field |
| Static parameters | ✅ Working | Flow end-to-end |
| Template resolver | ❌ Missing | Core implementation needed |
| Pack config loading | ❌ Missing | Required for `{{ pack.config }}` |
| Integration | ❌ Missing | Need to wire up resolver |
| Unit tests | ❌ Missing | Tests for resolver needed |
| Integration tests | ❌ Missing | E2E test needed |
| Documentation | ✅ Complete | User and tech docs done |
**Overall Status:** 📝 Documented, ⏳ Implementation Pending
**Priority:** P1 (High)
**Estimated Effort:** 2-3 days (MVP), 1-2 days (advanced features)
**Risk:** Low (backward compatible, well-scoped, clear requirements)
**Value:** High (unlocks production use cases, user expectation)
- [Rule Parameter Mapping Guide](./rule-parameter-mapping.md)
- [Rule Parameter Examples](../examples/rule-parameter-examples.md)
- [Rule Management API](../api/api-rules.md)
- [Executor Service Architecture](../architecture/executor-service.md)

View File

@@ -5,7 +5,7 @@
Rules in Attune can specify parameters to pass to actions when triggered. These parameters can be:
1. **Static values** - Hard-coded values defined in the rule
2. **Dynamic from trigger payload** - Values extracted from the event that triggered the rule
2. **Dynamic from event payload** - Values extracted from the event that triggered the rule
3. **Dynamic from pack config** - Values from the pack's configuration
This enables flexible parameter passing without hardcoding values or requiring custom code.
@@ -27,7 +27,7 @@ Rule `action_params` uses a JSON object where each value can be:
**Available Sources:**
- `trigger.payload.*` - Data from the event payload
- `event.payload.*` - Data from the event payload
- `pack.config.*` - Configuration values from the pack
- `system.*` - System-provided values (timestamp, execution context)
@@ -55,13 +55,13 @@ When this rule triggers, the action receives exactly these parameters.
---
## Dynamic Parameters from Trigger Payload
## Dynamic Parameters from Event Payload
Extract values from the event that triggered the rule.
### Example: Alert with Event Data
**Trigger Payload:**
**Event Payload:**
```json
{
"severity": "error",
@@ -84,10 +84,10 @@ Extract values from the event that triggered the rule.
"action_ref": "slack.post_message",
"action_params": {
"channel": "#incidents",
"message": "Error in {{ trigger.payload.service }}: {{ trigger.payload.message }}",
"severity": "{{ trigger.payload.severity }}",
"host": "{{ trigger.payload.metadata.host }}",
"timestamp": "{{ trigger.payload.timestamp }}"
"message": "Error in {{ event.payload.service }}: {{ event.payload.message }}",
"severity": "{{ event.payload.severity }}",
"host": "{{ event.payload.metadata.host }}",
"timestamp": "{{ event.payload.timestamp }}"
}
}
```
@@ -135,7 +135,7 @@ Use configuration values stored at the pack level (useful for API keys, URLs, et
"token": "{{ pack.config.api_token }}",
"channel": "{{ pack.config.default_channel }}",
"username": "{{ pack.config.bot_name }}",
"message": "{{ trigger.payload.message }}"
"message": "{{ event.payload.message }}"
}
}
```
@@ -160,8 +160,8 @@ Combine static and dynamic values in the same rule:
"action_params": {
"repo": "myorg/myrepo",
"token": "{{ pack.config.github_token }}",
"title": "Error: {{ trigger.payload.message }}",
"body": "Service {{ trigger.payload.service }} reported an error at {{ trigger.payload.timestamp }}",
"title": "Error: {{ event.payload.message }}",
"body": "Service {{ event.payload.service }} reported an error at {{ event.payload.timestamp }}",
"labels": ["bug", "automated"],
"assignees": ["oncall"]
}
@@ -177,11 +177,11 @@ Access nested properties using dot notation:
```json
{
"action_params": {
"user_id": "{{ trigger.payload.user.id }}",
"user_name": "{{ trigger.payload.user.profile.name }}",
"user_id": "{{ event.payload.user.id }}",
"user_name": "{{ event.payload.user.profile.name }}",
"metadata": {
"ip_address": "{{ trigger.payload.request.client_ip }}",
"user_agent": "{{ trigger.payload.request.headers.user_agent }}"
"ip_address": "{{ event.payload.request.client_ip }}",
"user_agent": "{{ event.payload.request.headers.user_agent }}"
}
}
}
@@ -196,8 +196,8 @@ Access array elements by index:
```json
{
"action_params": {
"first_error": "{{ trigger.payload.errors.0 }}",
"primary_tag": "{{ trigger.payload.tags.0 }}"
"first_error": "{{ event.payload.errors.0 }}",
"primary_tag": "{{ event.payload.tags.0 }}"
}
}
```
@@ -211,8 +211,8 @@ Provide default values when the referenced field doesn't exist:
```json
{
"action_params": {
"priority": "{{ trigger.payload.priority | default: 'medium' }}",
"assignee": "{{ trigger.payload.assignee | default: 'unassigned' }}"
"priority": "{{ event.payload.priority | default: 'medium' }}",
"assignee": "{{ event.payload.assignee | default: 'unassigned' }}"
}
}
```
@@ -226,10 +226,10 @@ Template values preserve their JSON types:
```json
{
"action_params": {
"count": "{{ trigger.payload.count }}", // Number: 42
"enabled": "{{ trigger.payload.enabled }}", // Boolean: true
"tags": "{{ trigger.payload.tags }}", // Array: ["a", "b"]
"metadata": "{{ trigger.payload.metadata }}" // Object: {"key": "value"}
"count": "{{ event.payload.count }}", // Number: 42
"enabled": "{{ event.payload.enabled }}", // Boolean: true
"tags": "{{ event.payload.tags }}", // Array: ["a", "b"]
"metadata": "{{ event.payload.metadata }}" // Object: {"key": "value"}
}
}
```
@@ -261,8 +261,8 @@ Embed multiple values in a single string:
```json
{
"action_params": {
"message": "User {{ trigger.payload.user_id }} performed {{ trigger.payload.action }} at {{ system.timestamp }}",
"subject": "[{{ trigger.payload.severity | upper }}] {{ trigger.payload.service }} Alert"
"message": "User {{ event.payload.user_id }} performed {{ event.payload.action }} at {{ system.timestamp }}",
"subject": "[{{ event.payload.severity | upper }}] {{ event.payload.service }} Alert"
}
}
```
@@ -276,10 +276,10 @@ Apply transformations to values:
```json
{
"action_params": {
"uppercase_name": "{{ trigger.payload.name | upper }}",
"lowercase_email": "{{ trigger.payload.email | lower }}",
"formatted_date": "{{ trigger.payload.timestamp | date: '%Y-%m-%d' }}",
"truncated": "{{ trigger.payload.message | truncate: 100 }}"
"uppercase_name": "{{ event.payload.name | upper }}",
"lowercase_email": "{{ event.payload.email | lower }}",
"formatted_date": "{{ event.payload.timestamp | date: '%Y-%m-%d' }}",
"truncated": "{{ event.payload.message | truncate: 100 }}"
}
}
```
@@ -310,19 +310,19 @@ Apply transformations to values:
"action_params": {
"channel": "{{ pack.config.alert_channel }}",
"token": "{{ pack.config.slack_token }}",
"message": "⚠️ Alert from {{ trigger.payload.source }}: {{ trigger.payload.message }}",
"message": "⚠️ Alert from {{ event.payload.source }}: {{ event.payload.message }}",
"attachments": [
{
"color": "{{ trigger.payload.severity | default: 'warning' }}",
"color": "{{ event.payload.severity | default: 'warning' }}",
"fields": [
{
"title": "Service",
"value": "{{ trigger.payload.service }}",
"value": "{{ event.payload.service }}",
"short": true
},
{
"title": "Environment",
"value": "{{ trigger.payload.environment | default: 'production' }}",
"value": "{{ event.payload.environment | default: 'production' }}",
"short": true
}
],
@@ -349,7 +349,7 @@ Apply transformations to values:
"token": "{{ pack.config.jira_token }}"
},
"issuetype": "Bug",
"summary": "[{{ trigger.payload.severity }}] {{ trigger.payload.service }}: {{ trigger.payload.message }}",
"summary": "[{{ event.payload.severity }}] {{ event.payload.service }}: {{ event.payload.message }}",
"description": {
"type": "doc",
"content": [
@@ -358,14 +358,14 @@ Apply transformations to values:
"content": [
{
"type": "text",
"text": "Error Details:\n\nService: {{ trigger.payload.service }}\nHost: {{ trigger.payload.host }}\nTimestamp: {{ trigger.payload.timestamp }}\n\nStack Trace:\n{{ trigger.payload.stack_trace }}"
"text": "Error Details:\n\nService: {{ event.payload.service }}\nHost: {{ event.payload.host }}\nTimestamp: {{ event.payload.timestamp }}\n\nStack Trace:\n{{ event.payload.stack_trace }}"
}
]
}
]
},
"priority": "{{ trigger.payload.priority | default: 'Medium' }}",
"labels": ["automated", "{{ trigger.payload.service }}"]
"priority": "{{ event.payload.priority | default: 'Medium' }}",
"labels": ["automated", "{{ event.payload.service }}"]
}
}
```
@@ -382,17 +382,17 @@ Apply transformations to values:
"routing_key": "{{ pack.config.pagerduty_routing_key }}",
"event_action": "trigger",
"payload": {
"summary": "{{ trigger.payload.metric_name }} exceeded threshold on {{ trigger.payload.host }}",
"summary": "{{ event.payload.metric_name }} exceeded threshold on {{ event.payload.host }}",
"severity": "critical",
"source": "{{ trigger.payload.host }}",
"source": "{{ event.payload.host }}",
"custom_details": {
"metric": "{{ trigger.payload.metric_name }}",
"current_value": "{{ trigger.payload.current_value }}",
"threshold": "{{ trigger.payload.threshold }}",
"duration": "{{ trigger.payload.duration_seconds }}s"
"metric": "{{ event.payload.metric_name }}",
"current_value": "{{ event.payload.current_value }}",
"threshold": "{{ event.payload.threshold }}",
"duration": "{{ event.payload.duration_seconds }}s"
}
},
"dedup_key": "{{ trigger.payload.host }}_{{ trigger.payload.metric_name }}"
"dedup_key": "{{ event.payload.host }}_{{ event.payload.metric_name }}"
}
}
```
@@ -431,9 +431,12 @@ Apply transformations to values:
1. **Rule Evaluation** - When an event matches a rule
2. **Template Extraction** - Identify `{{ }}` patterns in `action_params`
3. **Context Building** - Assemble available data:
- `trigger.payload` - Event payload data
- `event.id` - Event database ID
- `event.trigger` - Trigger ref that generated the event
- `event.created` - Event creation timestamp
- `event.payload` - Event payload data
- `pack.config` - Pack configuration
- `system.*` - System-provided values
- `system.*` - System-provided values (timestamp, rule info)
4. **Value Resolution** - Extract values from context using dot notation paths
5. **Type Conversion** - Preserve JSON types (string, number, boolean, object, array)
6. **Parameter Assembly** - Build final parameter object
@@ -444,7 +447,7 @@ Apply transformations to values:
**Missing Values:**
- If a referenced value doesn't exist and no default is provided, use `null`
- Log warning: `"Template reference not found: trigger.payload.missing_field"`
- Log warning: `"Template variable not found: event.payload.missing_field"`
**Invalid Syntax:**
- If template syntax is invalid, log error and use the raw string
@@ -506,8 +509,8 @@ Pack configuration should be stored securely and can include:
```json
{
"action_params": {
"priority": "{{ trigger.payload.priority | default: 'medium' }}",
"assignee": "{{ trigger.payload.assignee | default: 'unassigned' }}"
"priority": "{{ event.payload.priority | default: 'medium' }}",
"assignee": "{{ event.payload.assignee | default: 'unassigned' }}"
}
}
```
@@ -516,8 +519,8 @@ Pack configuration should be stored securely and can include:
```json
{
"action_params": {
"user_email": "{{ trigger.payload.user.email }}",
"user_id": "{{ trigger.payload.user.id }}"
"user_email": "{{ event.payload.user.email }}",
"user_id": "{{ event.payload.user.id }}"
}
}
```
@@ -528,7 +531,7 @@ If a value never changes, keep it static:
{
"action_params": {
"service_name": "my-service", // Static - never changes
"error_code": "{{ trigger.payload.code }}" // Dynamic - from event
"error_code": "{{ event.payload.code }}" // Dynamic - from event
}
}
```
@@ -581,10 +584,12 @@ Look for the resolved parameters in the execution's `config` field:
{
"id": 1,
"config": {
"message": "Test message", // Resolved from trigger.payload.message
"severity": "info", // Resolved from trigger.payload.severity
"user_id": 123, // Resolved from trigger.payload.user.id
"user_name": "Alice" // Resolved from trigger.payload.user.name
"message": "Test message", // Resolved from event.payload.message
"severity": "info", // Resolved from event.payload.severity
"user_id": 123, // Resolved from event.payload.user.id
"user_name": "Alice" // Resolved from event.payload.user.name
"event_id": 456, // Resolved from event.id
"trigger": "core.test_event" // Resolved from event.trigger
}
}
```
@@ -608,7 +613,7 @@ Look for the resolved parameters in the execution's `config` field:
```json
{
"action_params": {
"message": "Error: {{ trigger.payload.message }}"
"message": "Error: {{ event.payload.message }}"
}
}
```
@@ -679,15 +684,15 @@ Look for the resolved parameters in the execution's `config` field:
```json
{
"action_params": {
"channel": "{% if trigger.payload.severity == 'critical' %}#incidents{% else %}#monitoring{% endif %}"
"channel": "{% if event.payload.severity == 'critical' %}#incidents{% else %}#monitoring{% endif %}"
}
}
```
### 2. Advanced Filters
- Mathematical operations: `{{ trigger.payload.value | multiply: 100 }}`
- String manipulation: `{{ trigger.payload.text | replace: 'old', 'new' }}`
- Array operations: `{{ trigger.payload.items | join: ', ' }}`
- Mathematical operations: `{{ event.payload.value | multiply: 100 }}`
- String manipulation: `{{ event.payload.text | replace: 'old', 'new' }}`
- Array operations: `{{ event.payload.items | join: ', ' }}`
### 3. Custom Functions
```json
@@ -695,7 +700,7 @@ Look for the resolved parameters in the execution's `config` field:
"action_params": {
"timestamp": "{{ now() }}",
"uuid": "{{ uuid() }}",
"hash": "{{ hash(trigger.payload.data) }}"
"hash": "{{ hash(event.payload.data) }}"
}
}
```
@@ -704,7 +709,7 @@ Look for the resolved parameters in the execution's `config` field:
```json
{
"action_params": {
"user": "{{ trigger.payload.user | merge: pack.config.default_user }}"
"user": "{{ event.payload.user | merge: pack.config.default_user }}"
}
}
```
@@ -734,7 +739,8 @@ Rule parameter mapping provides a powerful way to:
**Key Concepts:**
- Static values for constants
- `{{ trigger.payload.* }}` for event data
- `{{ event.payload.* }}` for event payload data
- `{{ event.id }}`, `{{ event.trigger }}`, `{{ event.created }}` for event metadata
- `{{ pack.config.* }}` for pack configuration
- `{{ system.* }}` for system-provided values
- Filters and defaults for robust templates

View File

@@ -181,12 +181,12 @@ Both `trigger_params` and `conditions` can filter events, but they serve differe
},
"conditions": {
"and": [
{"var": "trigger.payload.status_code", ">=": 500},
{"var": "trigger.payload.retry_count", ">": 3},
{"var": "event.payload.status_code", ">=": 500},
{"var": "event.payload.retry_count", ">": 3},
{
"or": [
{"var": "trigger.payload.endpoint", "in": ["/auth", "/payment"]},
{"var": "trigger.payload.customer_impact", "==": true}
{"var": "event.payload.endpoint", "in": ["/auth", "/payment"]},
{"var": "event.payload.customer_impact", "==": true}
]
}
]
@@ -448,7 +448,7 @@ This improves performance by filtering earlier in the evaluation pipeline.
"action_ref": "slack.post_message",
"action_params": {
"channel": "#pull-requests",
"message": "New PR: {{ trigger.payload.title }} by {{ trigger.payload.user }}"
"message": "New PR: {{ event.payload.title }} by {{ event.payload.user }}"
}
}
```