Ask a standard NPS or CSAT question inside your product and let Fillo compute the score. You end with a survey that asks for the reason behind a score and counts one answer per person for each cycle or interaction.
Choose NPS or CSAT
| NPS | CSAT | |
|---|---|---|
| Question | How likely are you to recommend us? | How satisfied were you with this interaction? |
| Measures | Loyalty to the product overall | Satisfaction with one moment |
| Field | linear_scale from 0 to 10 | rating or linear_scale from 1 to 5 |
insightsMetric | "nps" | "csat" |
| Score in Fillo | % of 9–10 answers minus % of 0–6 answers | % of 4–5 answers |
| When to ask | On a schedule, such as each quarter | Right after the interaction |
| One answer per | Person and survey cycle | Person and interaction ID |
Fillo scores a field only when its scale matches. A 1 to 5 rating marked as NPS gets no NPS score.
Add an NPS survey
import { defineForm } from "@usefillo/react";
export const nps = defineForm({
id: "quarterly-nps",
title: "How are we doing?",
pages: [{
id: "score",
blocks: [
{ id: "nps_cycle", kind: "hidden", label: "Survey cycle" },
{
id: "recommend",
kind: "linear_scale",
label: "How likely are you to recommend us?",
min: 0,
max: 10,
minLabel: "Not likely",
maxLabel: "Very likely",
insightsMetric: "nps",
required: true,
},
{
id: "why",
kind: "long_text",
label: "What's the main reason for your score?",
visibleIf: [{ fieldId: "recommend", op: "answered" }],
},
],
}],
settings: {
submitLabel: "Send feedback",
successTitle: "Thanks for the feedback",
responseLimit: { by: "identify", scopeField: "nps_cycle", onRepeat: "keep" },
},
});Render it for a signed-in user and pass the current cycle:
<FilloForm
form={nps}
client={client}
respondent={respondent} // { id, email, hash }, hashed on your server
initialData={{ nps_cycle: "2026-q4" }}
/>Add a CSAT survey
import { defineForm } from "@usefillo/react";
export const csat = defineForm({
id: "support-csat",
title: "Rate your support experience",
pages: [{
id: "feedback",
blocks: [
{ id: "ticket_id", kind: "hidden", label: "Ticket", paramName: "ticket" },
{
id: "satisfaction",
kind: "rating",
label: "How satisfied are you with the help you received?",
max: 5,
insightsMetric: "csat",
required: true,
},
{
id: "improve",
kind: "long_text",
label: "What could we improve?",
visibleIf: [{ fieldId: "satisfaction", op: "lt", value: 4 }],
},
],
}],
settings: {
submitLabel: "Send rating",
successTitle: "Feedback received",
responseLimit: { by: "identify", scopeField: "ticket_id", onRepeat: "keep" },
},
});Name the thing being rated in the label, such as the support case, the purchase, or the onboarding step. Pass the ID with initialData={{ ticket_id }} in your app, or as ?ticket= on an emailed link.
Limit repeat answers
by: "identify" counts a signed-in person, verified by the hash your server computes. scopeField allows one answer per person for each value. One customer can rate several tickets, but each ticket once.
An identify limit rejects submissions it can't tie to a person. Use it only where the form knows who is answering. See respondents and identity for the hash.
Why the hidden ID isn't proof
A visitor can change a URL parameter or a hidden value. The hidden ID scopes and routes the answer. The signed respondent is what ties it to a real account.
Act on the scores
Insights shows the score and the distribution for each marked field. Read low scores with their written reason before anyone follows up.
For support teams, Slack sends a response link by default. You can add up to three answer fields to the message, such as the score and the reason.
Related
- NPS example: Try the live scale, the conditional follow-up, and the success state.
- Linear scale field: Keep NPS bounds and labels valid.
- Rating field: Configure a CSAT rating.
- Limit responses: Scope one answer to each cycle or interaction.