Blueprint example · Real-time activity

Real-time activity context for governed agents

A refund agent can stay under every single-refund limit while the total paid to one customer, or to one account, climbs within minutes. A check that looks only at the step in front of it cannot see that pattern. The pattern lives in your stream of business events.

This blueprint walks through a worked example at a retail bank: an autonomous agent proposes customer refunds, a stream-processing system maintains rolling totals over recent transactions, and Agent Control Room decides each refund before money moves. Nussknacker is used for illustration; any system that keeps rolling totals and can answer a lookup fits the same place.

An architecture pattern, not a packaged integration: there is no connector between the two products. Names of facts, thresholds and amounts are illustrative.

The blind spot of one-step checks

Agent Control Room already counts the permits it issues itself, so a rule can cap how often it grants. What it cannot see is everything else: refunds issued by staff, through other channels, or by systems it does not govern.

Nussknacker lets risk and operations teams build real-time algorithms (called scenarios in Nussknacker) over event streams with a visual editor, running on Apache Flink and optionally over Kafka. Here it maintains two numbers: how much this customer has been refunded in the last five minutes, and how many refunds this destination account has received in the last hour.

Architecture at a glance

  1. Bank event stream

    Payments, refunds, chargebacks and account events, as they happen.

  2. Nussknacker — real-time activity

    Sliding windows: refund total per customer (last five minutes), refund count per destination account (last hour). Each change updates a key-value store, queried directly or via an OpenAPI service by the bank's refund service.

  3. Bank refund service — enforcement point

    Receives the refund the agent proposes. Reads live totals from the lookup store and identity from the token, fixes them for this step, fingerprints the refund.

  4. Agent Control Room — decision point

    Applies the bank's ordered rules: allow, refuse, or ask a person first. Signs a short-lived permit bound to the refund's fingerprint.

  5. Bank refund service — acts

    Verifies the permit, pays through the core system and reports the outcome. The executed refund becomes a new event in the stream.

No direct connection, by design

Nussknacker and Agent Control Room never talk to each other. The bank's refund service is the only component that talks to both. Agent Control Room holds no credentials for the bank's systems and never queries them; it judges the facts it is sent.

Why doesn't the decision point query the stream itself?

Classic access-control diagrams let the decision point fetch missing facts. Agent Control Room deliberately does not. Letting an outside service reach into the bank's network would mean opening it to inbound calls and handing over credentials. Instead, the refund service, already inside the bank, reads the live totals and sends them with the request, so the bank keeps every connection and credential, and each decision is recorded with exactly the facts it was based on.

Who does what

In standard access-control terms (NIST SP 800-162), the parts map onto three roles. The two policy information points (PIP) are where facts come from; the enforcement point (PEP) reads them; Agent Control Room is the decision point (PDP) and deliberately not an information point.

Enforcement point (PEP)
Bank refund service
Intercepts every refund the agent proposes, gathers trusted facts, asks for a decision, verifies the permit, pays and reports the outcome. Holds the payment credentials.
Information point — real-time activity (PIP)
Nussknacker, with the bank's key-value store
Nussknacker keeps sliding-window totals per customer and per destination account from the bank's events and writes each change straight to a bank-owned key-value store. That store answers the refund service with the current value and how far it has caught up with the stream.
Information point — identity (PIP)
Bank identity provider
Signs in people and services. The refund service validates the token and turns its verified claims into facts.
Decision point (PDP)
Agent Control Room
Applies the bank's ordered rules to the facts, answers allow, refuse or ask a person first, signs the permit and keeps the decision record.

One refund, end to end

  1. Step 0

    Set up once

    The bank's stream team builds two scenarios in the Nussknacker Designer: a five-minute sliding total of refunds per customer, and a one-hour count of refunds per destination account. Each uses a sliding-window aggregate and updates a bank-owned key-value store with every change. Emitting when an event leaves the window means the total is updated downward as old refunds age out. With Nussknacker, the bank's risk team can change what is collected as context for policy — small changes such as a different window length, or additional context data — without changing the agent's code.

    A bank administrator declares the facts the refund action expects in Agent Control Room — its context contract — including who may assert them. The asserter, "bank.live-context", names the bank-owned store the Nussknacker scenarios keep current; only values the refund service reads from that store may carry it, never values the agent supplies.

    Nussknacker scenarios, as built in the Designer (topic and field names illustrative)
    Scenario customer-refund-total-5m
    (streaming)
    
    kafka source
      topic: bank.refunds
    filter
      status == "EXECUTED"
    aggregate-sliding
      groupBy: customerRef
      aggregator: Sum
      aggregateBy: amount
      windowLength: 5 minutes
      emitWhenEventLeft: true
    key-value store sink
      store: bank.live-context
      key: customerRef
    
    Scenario destination-refund-count-1h
      the same shape, with
      groupBy: destinationRef
      aggregator: Count
      windowLength: 1 hour
    Context contract for refund.issue in Agent Control Room
    action refund.issue — contract mode: enforced
    
    effect.amount
      number, EUR, required
    
    activity.customer_refund_total_5m
      number, EUR, required
      provenance required, asserted by "bank.live-context"
    
    activity.destination_refund_count_1h
      whole number, required
      provenance required, asserted by "bank.live-context"
  2. Step 1

    The agent proposes a refund

    A customer-service agent concludes that a customer is owed €150 for a duplicate charge and calls its refund tool. The call reaches the bank's refund service, not the payment system: the agent holds no payment credentials of its own.

  3. Step 2

    The refund service gathers trusted facts

    The refund service separates what the agent proposed from what it can vouch for itself:

    • The step itself: the refund the agent proposed, validated by the service — amount, customer and destination account. This is what will be paid, so its amount becomes a fact and the whole refund is fingerprinted.
    • Identity, from the validated service token: which agent and service is asking.
    • Real-time activity, from the lookup store Nussknacker keeps current: this customer has already received €220 in refunds in the last five minutes; the destination account has received two refunds in the last hour. Each total comes with its own observation time.
    Refund service (TypeScript, trimmed)
    // Inside the bank's refund service — the enforcement point.
    // The agent proposes; the service validates. Trusted facts
    // never come from what the agent wrote.
    const caller = await verifyServiceToken(req.headers.authorization); // identity
    const refund = validateRefund(proposal); // the exact refund to pay
    
    // liveTotals is a bank helper over the bank-owned key-value store
    // that the Nussknacker scenarios keep current — not a
    // Nussknacker API. Each read returns { value, asOf }: value is 0
    // when the window holds nothing; asOf is how far the store has
    // caught up with that scenario's stream.
    const total5m = await liveTotals.get("customer-refund-total-5m", refund.customerRef);
    const count1h = await liveTotals.get("destination-refund-count-1h", refund.destinationRef);
    
    const context = {
      "effect.amount": refund.amount,
      "activity.customer_refund_total_5m": {
        value: total5m.value,
        assertedBy: "bank.live-context",
        observedAt: total5m.asOf,
      },
      "activity.destination_refund_count_1h": {
        value: count1h.value,
        assertedBy: "bank.live-context",
        observedAt: count1h.asOf,
      },
    };
    const payloadHash = await sha256Hex(canonicalJson(refund)); // the exact refund

    The observation time says how current the store is, not when this customer's total last changed: a customer with no recent refunds has a total of zero that is just as current. Retries of this step send the same values unchanged.

  4. Step 3

    The refund service asks Agent Control Room

    Agent Control Room receives numbers, a pseudonymous reference and a fingerprint — not the customer's name, account number or the refund details themselves.

    The decision request
    decideAction
    {
      "agent": "refund-agent",
      "action": "refund.issue",
      "resourceRef": "customer:7f3a9c",
      "payloadHash": "9b1c…e04a",
      "context": {
        "effect.amount": 150,
        "activity.customer_refund_total_5m": {
          "value": 220,
          "assertedBy": "bank.live-context",
          "observedAt": "2026-10-23T09:12:04Z"
        },
        "activity.destination_refund_count_1h": {
          "value": 2,
          "assertedBy": "bank.live-context",
          "observedAt": "2026-10-23T09:12:03Z"
        }
      }
    }
  5. Step 4

    Agent Control Room applies the bank's rules

    Because the contract is enforced, the facts are checked first: present, the right kind, asserted by the named source. Then the rules run in order; the first match wins, and anything unmatched is refused.

    #WhenAnswer
    1The destination account has received 5 or more refunds in the last hourRefuse
    2The customer has already received more than €150 in the last five minutesAsk a person first
    3This refund is above €200Ask a person first
    4refund-agent asks for refund.issueAllow
    —Anything elseRefuse (default)

    Here the single refund is small, but the customer has already received €220 in five minutes, so rule 2 applies: ask a person first. Had the customer received nothing recently, rule 4 would have allowed the refund at once, without waiting for anyone — the usual path for routine refunds.

  6. Step 5

    A person decides; the service claims, acts and reports

    • The refund waits in Agent Control Room's approval queue, and a refunds officer approves or refuses it.
    • The refund service polls getApprovalStatus. When it reads granted, it reads the live totals again and calls decideAction once more, within fifteen minutes. If the totals are unchanged, only their observation times are newer, and the call claims the signed, single-use permit: an approval is tied to the values the person saw, not to their timestamps.
    • If a total has changed meanwhile — another refund landed — it is a different question: the rules run on the new figures, and a person is asked again if they still call for one.
    • It verifies the permit's signature and that its fingerprint matches the refund it is about to pay, then pays through the core system.
    • It reports the outcome with reportExecutionOutcome: executed, aborted or downstream_error. The executed refund enters the event stream and the window updates.

decideActiongetApprovalStatusgetGrantVerificationKeyreportExecutionOutcome

Engineering points to consider

Freshness

A live aggregate is useful only if the system can establish how current it is. This is not necessarily the time when the aggregate last changed: a value may legitimately remain unchanged while the stream continues to progress. How freshness is established therefore needs explicit consideration. In Flink-based processing, event time and watermarks provide a useful notion of progress and may form part of such a mechanism. If the aggregate is served from an external lookup store, however, the freshness definition also needs to account for the path between the stream processor and that store. The required freshness bound should be treated as an application-specific requirement and validated against the end-to-end processing latency.

Refunds in flight

Two refunds evaluated moments apart can both see the same total, because the first has not reached the stream yet. The refund service can add its own in-flight amounts to the looked-up total, or the stream can expose pending refunds as a separate value. As a backstop, an Agent Control Room rule can cap how many permits it grants per window; those caps are counted atomically on its side.

When the lookup fails

If the real-time activity lookup cannot answer, the refund service sends no real-time activity fact and the enforced contract leaves the step not decided — no refund is paid on a guess. The service can try again shortly or hand the case to staff outside the agent's path; that choice belongs to the bank.

Who vouches for a fact

Agent Control Room records which service asserted a fact and when, but it cannot prove either. Trust rests on the refund service taking the value from the lookup store the stream keeps current, never from the agent.

Why both

Agent Control Room already caps how often it grants. Real-time activity adds what it cannot see: refunds by staff, through other channels, or by systems it does not govern.

Added time

The lookup is one extra internal call in the refund path. No figure is assumed here; a test measures it.