Core Concepts
This document defines the key abstractions in Swiftward and how they interact.
Event
An Event is the input to policy evaluation. It represents something that happened: a user posted content, an AI generated output, a PR was opened.
{
"id": "evt_abc123",
"entity_id": "user_456",
"type": "ugc.post.created",
"data": {
"text": "Check out this link...",
"channel": "comments"
},
"meta": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0..."
},
"timestamp": "2025-01-15T10:30:00Z"
}
| Field | Purpose |
|---|---|
id |
Unique identifier; used for idempotency |
entity_id |
Partitioning key (e.g., user_id); ensures ordering |
type |
Routing and policy matching |
data |
Event payload; accessed in rules via event.data.* paths |
meta |
Contextual info; accessed via event.meta.* paths |
Entity
An Entity is the subject of policy enforcement — typically a user, session, or resource. State (labels, counters, metadata) is stored per entity.
Entities are identified by entity_id in events. All events with the same entity_id are processed in order and share state.
Policy Version
A Policy Version is a complete, immutable snapshot of your policy configuration: constants, signals, rules, action profiles, and UDF profiles.
- Policies are versioned (v1, v2, v3...)
- One version is active at a time
- Switching versions is instant (no restart)
- Old versions retained for audit and rollback
Rule
A Rule defines conditions and their consequences. Rules are defined as a map with named keys.
dsl_version: 2
rules:
block_toxic_content:
enabled: true
mode: "both" # sync, async, or both
all: # Composite condition: all, any, none, not
- path: "event.type"
op: eq
value: "ugc.post.created"
- path: "signals.toxicity_score"
op: gt
value: "{{ constants.toxicity_threshold }}"
effects:
verdict: rejected
priority: 100
state_changes:
set_labels: ["toxic_content"]
change_counters:
toxic_count: 1
actions:
- action: notify_admin
params:
channel: "#alerts"
response: # Returned in sync mode
blocked: true
reason: "Content violates toxicity policy"
| Field | Purpose |
|---|---|
enabled |
Whether rule is active |
mode |
When to evaluate: sync, async, or both |
all/any/none/not |
Composite conditions (or leaf: path/op/value) |
effects.verdict |
Outcome: approved, rejected, flagged |
effects.priority |
Evaluation order (higher = first) |
effects.state_changes |
State mutations to apply |
effects.actions |
Side effects to execute after commit |
effects.response |
Data returned to caller in sync mode |
Condition Operators: eq, ne, gt, gte, lt, lte, in, not_in, contains, not_contains, regex_match, regex_not_match
Evaluation order: Rules are evaluated in priority order. The first matching rule determines the verdict (unless configured for accumulation).
Signal (UDF)
A Signal is a computed value derived from the event, state, or external calls. Signals are defined as User-Defined Functions (UDFs) using a category/name convention.
signals:
toxicity_score:
udf: llm/moderation
params:
text: "{{ event.data.text }}"
provider: openai
model: moderation
user_is_new:
udf: math/compare
params:
left: "{{ state.counters.post_count }}"
operator: "<"
right: 5
pii_detected:
udf: pii/scanner
params:
text: "{{ event.data.text }}"
types: ["email", "phone", "ssn"]
Common UDF Categories:
| Category | Examples | Purpose |
|---|---|---|
llm/ |
moderation, topic_classifier |
LLM-based content analysis |
pii/ |
scanner |
PII detection |
state/ |
label_exists |
Entity state checks |
math/ |
compare |
Numeric comparisons |
collection/ |
count, contains |
List operations |
scm/ |
path_match, glob_match |
File path matching (for SCM) |
security/ |
secret_scanner, prompt_injection_detector |
Security checks |
Signal DAG:
- Signals can depend on other signals
- Dependencies form a Directed Acyclic Graph (DAG)
- Cycle detection at policy load time
- Evaluation order determined by DAG topology
Caching and Sharing:
- Signals are computed on demand (lazy)
- Once computed, cached for the event's evaluation
- If multiple rules reference the same signal, it's computed once
Action Profile
An Action Profile defines a reusable action configuration that extends a base action type.
action_profiles:
slack_alert:
extends: webhook
params:
url: "{{ env.SLACK_WEBHOOK_URL }}"
method: POST
github_set_status:
extends: scm/publish_check
params:
provider: github
context: "swiftward/policy"
Actions are referenced in rules by profile name:
effects:
actions:
- action: slack_alert
params:
body:
text: "Alert from {{ event.entity_id }}"
channel: "#trust-safety"
Base Action Types:
| Type | Purpose |
|---|---|
webhook |
HTTP POST to external endpoint |
scm/publish_check |
Set PR check status (GitHub/GitLab) |
scm/request_review |
Request PR reviewers |
ban_user |
User enforcement action |
notify_admin |
Internal notification |
Separation from evaluation:
- Actions execute after state commit
- If action fails, state is not rolled back (effects already applied)
- Failed actions can be retried independently
- This separation ensures deterministic evaluation
State Changes (Effects)
State changes are mutations applied to entity state. They are computed during evaluation but applied atomically during commit.
effects:
state_changes:
set_labels: ["flagged", "needs_review"]
delete_labels: ["trusted"]
change_counters:
violations: 1
posts_today: 1
set_counters:
strikes: 0 # Reset to specific value
set_metadata:
last_violation: "2025-01-15"
reason: "toxicity"
delete_metadata: ["temp_flag"]
| Field | Description |
|---|---|
set_labels |
Add labels to entity's label set |
delete_labels |
Remove labels from entity's label set |
change_counters |
Increment counters by delta (can be negative) |
set_counters |
Set counters to specific values |
set_metadata |
Set key-value pairs |
delete_metadata |
Remove key-value pairs |
State changes are accumulated across all matching rules, then applied atomically.
Verdict
A Verdict is the outcome of policy evaluation for an event.
| Verdict | Purpose |
|---|---|
rejected |
Deny the action (highest priority) |
flagged |
Queue for review or special handling |
approved |
Permit the action (lowest priority) |
Verdict ordering: When multiple rules match (in accumulation mode), verdicts have priority: rejected > flagged > approved.
The response field in effects can include additional data returned to the caller, such as:
blocked: true/falsereason: "..."needs_redaction: truewith redaction instructionspending_review: true
Parameter Templating
Parameters in rules, signals, and actions use {{ ... }} syntax for dynamic values.
Resolution sources (in order):
| Source | Syntax | Example |
|---|---|---|
| Event data | {{ event.data.* }} |
{{ event.data.text }} |
| Event meta | {{ event.meta.* }} |
{{ event.meta.ip }} |
| Signals | {{ signals.* }} |
{{ signals.toxicity_score }} |
| State | {{ state.labels }}, {{ state.counters.* }}, {{ state.metadata.* }} |
{{ state.counters.post_count }} |
| Constants | {{ constants.* }} |
{{ constants.toxicity_threshold }} |
| Environment | {{ env.* }} |
{{ env.SLACK_TOKEN }} |
Templating is resolved at evaluation time. Missing values can have defaults: {{ event.data.channel | default: "unknown" }}.
Two-Phase Execution
Swiftward separates evaluation from side effects:
Phase 1: Evaluate (deterministic)
- Load event and entity state
- Compute signals (DAG order)
- Evaluate rules (priority order)
- Determine verdict and accumulate effects
- Build decision trace
Phase 2: Commit and Act
- Apply state changes transactionally (idempotent via event
id) - Write decision trace
- Execute actions (webhooks, notifications)
Why two phases?
- Determinism: Same event + same state + same policy = same verdict
- Replayability: Re-evaluate without re-executing actions
- Auditability: Trace captures evaluation independent of action success
- Testability: Test rules without triggering side effects
State Model
Each entity has isolated state:
| Type | Description | Example |
|---|---|---|
| Labels | Set of strings | ["verified", "high_risk", "vip"] |
| Counters | Named integers | {"post_count": 42, "flags": 3} |
| Metadata | Key-value pairs | {"last_review": "2025-01-10", "tier": "premium"} |
Lazy loading: State is loaded only if rules reference it.
Idempotency: Each event is processed exactly once per entity. The event id is recorded; duplicate events are skipped.
ACID: State mutations are applied in a transaction. If commit fails, the event is retried or sent to DLQ.
Decision Trace
Every evaluation produces an immutable Decision Trace:
{
"trace_id": "tr_xyz789",
"policy_version": "ugc_moderation_v1",
"id": "evt_abc123",
"entity_id": "user_456",
"signals": {
"toxicity_score": {
"value": 0.85,
"cached": false,
"duration_ms": 120,
"udf": "llm/moderation"
}
},
"rules_evaluated": [
{
"name": "block_toxic_content",
"priority": 100,
"matched": true,
"condition": {"all": [{"path": "signals.toxicity_score", "op": "gt", "value": 0.8}]},
"condition_result": true
}
],
"verdict": "rejected",
"verdict_source": "block_toxic_content",
"effects": {
"state_changes": {
"set_labels": ["toxic_content"],
"change_counters": {"toxic_count": 1}
},
"actions": [{"action": "notify_admin", "params": {...}}]
},
"duration_ms": 145,
"timestamp": "2025-01-15T10:30:00.145Z"
}
Traces enable:
- Audit: Why was this decision made?
- Debugging: Which signal values led to this verdict?
- Replay: Re-evaluate with different policy version
- Analytics: Aggregate patterns over time
Further reading: