---
title: "Ask a System One model what your users are doing right now | Snowplow Signals"
description: "System One models answer typed questions about a user in a fraction of a second. Where they fit in an app, how to feed them live behavior, and what goes wrong."
source: https://signals.snowplow.io/blog/ask-a-system-one-model-what-your-users-are-doing
---

[Blog](https://signals.snowplow.io/blog) / Use cases 

# Ask a System One model what your users are doing right now

![](https://signals.snowplow.io/images/authors/matus-tomlein.jpg) [Matúš Tomlein](https://signals.snowplow.io/blog/authors/matus-tomlein) , Principal Software Engineer Oct 8, 2026 · 5 min read 

Every app makes small decisions about the people using it. Should this subscriber see an offer before they cancel? Is this visitor in the middle of something, or is now a good time to ask a question? Which support queue should this chat go to? Most apps answer these with a rule someone wrote years ago, or don’t ask them at all.

Our [last post](https://signals.snowplow.io/blog/decisions-are-a-commodity-context-is-not) covered the new models built for these calls, and why the state you send them matters more than which one you pick. This one covers where they fit in an app, how to feed them what your users are doing right now, and what tends to go wrong after launch.

## Decisions rules never got right

A rule works when the condition is something you can count. “Show the banner after the third visit” is easy to write and easy to check. Whether someone is stuck, comparing products, or about to leave isn’t a number. Rules approximate it with proxies, such as time on page or the number of errors, and the proxies pile up as people find cases they miss.

A trained model can learn the difference, but only from labels. Someone has to mark thousands of sessions as “stuck” or “not stuck”, and when the product changes the question, the labeling starts over. A large language model needs no labels, but it is usually too slow and too costly to call on every page view.

A System One model, such as TypeSafe’s [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev), sits between those. You write the question and the allowed answers in plain language, send a state object, and get back a probability for each answer in a fraction of a second. To change the question, you edit a string.

* Rules: Counts and thresholds; Cheap enough for every page view. Breaks on anything you can't count.
* Trained model: Calls about what the user is doing; Cheap enough for every page view. Needs labels, and new ones when the question changes.
* System One model: Calls about what the user is doing; Cheap enough for every page view. Write the question and the answers, no labels.
* Large language model: Calls about what the user is doing; Too slow or costly for every page view. Needs no labels, but each answer is slow and costly.

Cost per answer

Cheap enough for every page view

Rules Breaks on anything you can't count 

Trained model Needs labels, and new ones when the question changes 

System One model Write the question and the answers, no labels 

Too slow or costly for every page view

Large language model Needs no labels, but each answer is slow and costly 

Counts and thresholds

Calls about what the user is doing

What it can decide

Thresholds and counts still belong in code. Where you have labels and a question that stays the same, a trained model is hard to beat. For everything else, ask whether a person could make the call in a few seconds by looking at what the user just did. If they could, a System One model probably can too.

## Six decisions worth asking a System One model

Each of these is a question about one person, asked at a specific moment. They are illustrations, but apps already make every one of these calls with rules today. The cards show when the question is asked, the allowed answers, and what the state needs. Attributes are counts and values that Signals keeps current for each user, and the [agentic context](https://signals.snowplow.io/learning/ai-agents-and-memory/what-is-agentic-context) is a short list of their recent events.

On a product page

What is this shopper trying to do?

* browsing
* comparing
* ready\_to\_buy

Attributes

Items in cart, searches this session

Agentic context

The products they just viewed

When a reader opens an article

Which wall should they see?

* registration
* subscribe
* none

Attributes

Articles read this month, newsletter signup

Agentic context

The topics they read today

On the cancel page

What would keep this subscriber?

* pause
* downgrade
* help
* none

Attributes

Months subscribed, features used this month

Agentic context

Errors and pages just before they clicked cancel

Before a survey or chat prompt

Is this visitor in the middle of a task?

* yes
* no

Attributes

Prompts already shown this session

Agentic context

Form fields filled, scrolling, idle time

When a support chat opens

Which queue, and how urgent?

* billing
* technical
* account
* urgency 1 to 5

Attributes

Plan, past tickets, failed payments

Agentic context

Pages viewed and errors hit before the chat

After key events in a trial

Is this account ready for sales?

* sales
* onboarding
* self\_serve

Attributes

Teammates invited, active days, features adopted

Agentic context

API keys created, pricing and docs pages read

## The state decides whether the answer is any good

Each of those calls depends on when you ask, what you ask, and the state you send. The state needs the most care. TypeSafe’s [notes on Jev’s weak spots](https://docs.typesafe.ai/model-jaggedness/jev-1.13) say it doesn’t count reliably, reads dates as text, and loses accuracy as the state fills with content the question doesn’t need. A raw clickstream runs into all three. A session can hold thousands of events, each with a timestamp, and most of them are page pings and impressions.

Signals builds a state from the session that avoids those weak spots.

1. **Attributes do the arithmetic.** Signals computes counts, recency and last values from each event as it arrives, so the model reads “used two parts of the product this month” and never has to count.
2. **The agentic context keeps the order.** It is a short list of the recent events a decision cares about, with only the properties you name. The model can see that a failed export came right before the billing page, and no count would show that.

01The session

1. page\_view Reports (kept)
2. page\_ping
3. banner\_impression
4. export\_clicked
5. export\_failed csv (kept)
6. page\_ping
7. export\_clicked
8. export\_failed csv (kept)
9. page\_ping
10. banner\_impression
11. page\_view Billing (kept)
12. page\_ping
13. cancel\_clicked (kept)

Most events say little about this decision.

02The state Signals serves

Attributes

plan

team

months\_subscribed

7 to 12

features\_used\_this\_month

2

failed\_payments

0

Agentic context

1. page\_view Reports
2. export\_failed csv
3. export\_failed csv
4. page\_view Billing
5. cancel\_clicked

03The answer

What would keep this subscriber?

help 0.74  (chosen) 

pause 0.12 

downgrade 0.09 

none 0.05 

Threshold 0.7

Below the threshold, the page keeps its default.

An illustration of the cancel-page call. Signals keeps the state current as events arrive, so your app reads it in one call.

The model reads a short state instead of the whole session. Your code gets back a probability for each answer and can check it before acting.

## Building the call in Node

In Node, the Signals SDK reads both parts of the state and the TypeSafe SDK asks the question. Here is a short version of the cancel-page call.

```
import { Signals } from "@snowplow/signals-node";
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";

const signals = new Signals({ baseUrl, apiKey, apiKeyId, organizationId });
const typesafe = new TypeSafeClient();

export async function cancelOffer(userId: string, sessionId: string) {
  const [subscriber, recent] = await Promise.all([
    signals.getServiceAttributes({
      name: "subscriber",
      attribute_key: "user_id",
      identifier: userId,
    }),
    signals.getAgenticContext({
      name: "recent_activity",
      identifier: sessionId,
      format: "json",
    }),
  ]);

  const { answers } = await typesafe.systemOne({
    model: "jev-1.13.0",
    state: { subscriber, recent_activity: recent.events },
    questions: {
      offer: choice("What would most likely keep this subscriber?", {
        pause: "They still value the product but have little use for it right now.",
        downgrade: "They use a few features and the price is the problem.",
        help: "Something is broken for them, such as repeated errors before canceling.",
        none: "They have moved on and an offer would only slow them down.",
      }),
    },
  });

  const { choice: offer, confidence } = answers.offer;
  return confidence >= 0.7 ? offer : "none";
}
```

Keep the last line. When the confidence is low, showing the default page is safer than acting on a guess.

> Your coding agent can add the Signals side for you. Run `npx plugins add snowplow/skills`, then ask it to add the Node SDK, define the attributes and the agentic context the call needs, and pass them as the state. The docs cover [attributes](https://docs.snowplow.io/docs/signals/applications/retrieve-attributes/) and [agentic contexts](https://docs.snowplow.io/docs/signals/applications/agentic-contexts/).

## What goes wrong once it ships

Writing the call is quick, and it will look right on the handful of states you try it with. The trouble starts when real visitors hit it, and none of it shows up as an error in your logs. These are the problems we would watch for. You can find most of them before launch by running the call on past sessions, but you need a live experiment to learn whether acting on the answer helps.

Two answers that mean the same thing

If pause and downgrade both fit a subscriber who barely uses the product, the probability splits between them and neither reaches 0.7.

Find it before launch 

Sure of itself with little to go on

A subscriber who clicked cancel straight from the home page can get an answer as confident as one with a week of failed exports behind them.

Find it before launch 

Tested only on tidy examples

The states you type into a test don't have the duplicate page views, missing fields and double-firing tags that real sessions do.

Find it before launch 

Asked at the wrong moment

On the first page view there is little to go on, and by the cancel click it may be too late to help.

Find it before launch 

Answers that change without a deploy

A new Jev release or a renamed event can shift your answers while your code stays the same.

Find it before you upgrade 

Not knowing whether it helped

Showing an offer changes what the subscriber does next, so past sessions can't tell you whether the offer worked.

Needs a live experiment 

TypeSafe’s own notes say Jev answers the question you wrote, not the one you meant. Two answers that read differently to you can look the same to the model, and you usually only notice after going through a lot of real answers. A high confidence doesn’t mean the answer is right, either. How closely confidence tracks accuracy varies between models and datasets, which is why a [study of Jev on police crash reports](https://arxiv.org/abs/2609.24052) recommends auditing each model separately.

To try it on your own app, [start free](https://signals.snowplow.io/start). The free tier runs on the real engine, needs no card, and shows an attribute updating against your own traffic in the first session. The [System One models page](https://signals.snowplow.io/use-cases/system-one-models) shows more decisions built on live behavior.

![](https://signals.snowplow.io/images/authors/matus-tomlein.jpg)

[Matúš Tomlein](https://signals.snowplow.io/blog/authors/matus-tomlein) 

Principal Software Engineer, Snowplow

Follow Matúš for more on real-time customer context.

[LinkedIn](https://www.linkedin.com/in/matustomlein) [X](https://x.com/matus%5Ftomlein) [More from Matúš → ](https://signals.snowplow.io/blog/authors/matus-tomlein) 

Start on the free tier

Add the SDK and watch an attribute updating against your own traffic in the first session. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) 

Share this post

✕

Click the image to zoom in. Esc closes. Scroll to pan. Click the image to zoom out. 

In this post

* [Decisions rules never got right](#decisions-rules-never-got-right)
* [Six decisions worth asking a System One model](#six-decisions-worth-asking-a-system-one-model)
* [The state decides whether the answer is any good](#the-state-decides-whether-the-answer-is-any-good)
* [Building the call in Node](#building-the-call-in-node)
* [What goes wrong once it ships](#what-goes-wrong-once-it-ships)

Start on the free tier

Add the SDK and watch an attribute updating against your own traffic in the first session. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start)
