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/false
  • reason: "..."
  • needs_redaction: true with redaction instructions
  • pending_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)

  1. Load event and entity state
  2. Compute signals (DAG order)
  3. Evaluate rules (priority order)
  4. Determine verdict and accumulate effects
  5. Build decision trace

Phase 2: Commit and Act

  1. Apply state changes transactionally (idempotent via event id)
  2. Write decision trace
  3. 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: