Docs

.NET and Blazor quickstart

The WordsAreFlowing.Sdk package targets .NET 10 and ships a Blazor component for the private ask. Its default storage is the browser's localStorage, through Blazor WebAssembly.

1. Install

dotnet add package WordsAreFlowing.Sdk

The packages are on nuget.org. WordsAreFlowing.Sdk brings in WordsAreFlowing.Sdk.Core, the engine every head shares.

2. Register

In Program.cs, call AddWordsAreFlowing with your product's key. The options are a WordsAreFlowingOptions; ApiKey is the only required one, and registration throws an ArgumentException without it.

using WordsAreFlowing.Sdk;

builder.Services.AddWordsAreFlowing(o =>
{
    o.ApiKey = "waf_live_…";
    o.AppVersion = "1.4.0";
});

This registers an IFeedbackLoop, the SDK's one object. Every host-specific piece (storage, clock, prompter, transport) is registered with TryAdd, so a registration you make before this call wins.

Not on Blazor WebAssembly? The default store needs WebAssembly's in-process JavaScript runtime and throws elsewhere (Blazor Server, for instance). Register your own IKeyValueStore before calling AddWordsAreFlowing. InMemoryKeyValueStore exists, but it keeps nothing between runs, so occurrence counts and cooldowns start over each time.

3. Show the private ask

Put the FeedbackAsk component once in your layout and hand it the loop.

@using WordsAreFlowing.Sdk.Components
@using WordsAreFlowing.Sdk.Core.Engine
@inject IFeedbackLoop Loop

<FeedbackAsk Loop="@Loop" />

The component listens to DecisionMade and opens only when a decision asks for it. It sends the note with SubmitFeedbackAsync, and "Not now" calls DismissAskAsync. It never calls TrackAsync, so it can never cause a store prompt. Its copy is in English or French, following the current UI culture; pass a FeedbackAskTexts as Texts to change any string.

On its first render the component also calls InitializeAsync, which loads the cached rules and then fetches fresh ones if the network allows. If you set InitializeOnFirstRender="false", call it yourself; it is safe to call more than once. A rules change you make in the portal reaches a device the next time InitializeAsync runs there.

4. Report triggers

Call TrackAsync where a behavioural moment happens in your code — after the thing is done, not from a "rate us" button.

private async Task OnWorkoutSaved()
{
    // ... after the save succeeded
    var decision = await Loop.TrackAsync("workout_completed");
    if (decision.Kind == DecisionKind.None)
        Console.WriteLine($"Words Are Flowing: {decision.Reason}");
}

It counts one occurrence on the device, asks the engine, and returns a Decision: its Kind is a DecisionKind (None, AskRequested or StorePromptFired), and for None its Reason names the rule that blocked — the full list is in the rules reference. It does not throw: a failure inside the loop comes back as None with the reason error.

Your own ask instead of the component

Loop.DecisionMade += d =>
{
    if (d.Kind == DecisionKind.AskRequested) ShowMyAsk(d.Trigger);
};

// when the user sends (rating is 1 to 5, or null)
var result = await Loop.SubmitFeedbackAsync(trigger, message, rating);
if (!result.Ok) ShowError(result.Code);

// when the user closes it without writing
await Loop.DismissAskAsync(trigger);

DecisionMade is raised after every TrackAsync, including None decisions. SubmitFeedbackAsync returns a TransportResult; a failed note is not retried for you, so keep what the user wrote and let them try again. When Ok is false, Code carries the server's code when it refused the note (for example feedback.empty for a blank message, or feedback.too_long past 2,000 characters).

No store prompt on the web

A browser has no native review prompt. On the web a store-prompt rule whose conditions are met ends in None with the reason prompter_unavailable. If you want to point web users at your store listing, render a plain link with StoreLinks — never a button that claims to open a review prompt.

var ids = Loop.CurrentRules?.StoreIds;
var play = StoreLinks.GooglePlay(ids?.AndroidPackage);  // null when not set
var appStore = StoreLinks.AppStore(ids?.IosAppId);

The store ids come from the product in the portal and travel inside the rules, available as CurrentRules once InitializeAsync has run.

Options

OptionMeaning
ApiKeyRequired. The product's key.
AppVersionSent with every note.
UserIdOptional. Your own identifier for the user, sent with every note so you can find or delete their notes. Up to 128 characters from A–Z a–z 0–9 _ . : @ -.
AttributesOptional. Key/value pairs sent with every note and matched by rule conditions. Up to 20 keys; a value is a string (up to 200 characters), a boolean or a number.
MaxStorePromptsPerYearLowers the store-prompt cap for this app. It can never raise it above 3.
FlushThresholdCounters are posted when this many distinct counters are queued. Default 10. FlushAsync posts them now, unless a flush failed in the last five minutes.
BaseAddressThe ingest API. Defaults to the hosted service.
PlatformReported on notes. Defaults to web.

Counters that cannot reach the server stay queued, and the SDK makes no new attempt for five minutes. A note that is sent successfully posts the queue with it, even inside those five minutes.