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

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 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, 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.

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 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 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?

help0.74 (chosen)
pause0.12
downgrade0.09
none0.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 and 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 recommends auditing each model separately.

To try it on your own app, start free. 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 shows more decisions built on live behavior.

Matúš Tomlein

Principal Software Engineer, Snowplow