Skip to main content

Custom KPI

Build your own KPIs with the Feedier formula builder: every function, parameter and operator, ready-to-use formulas and troubleshooting.

J
Written by Julien Chil Hagopian

A custom KPI is a formula you write once and then reuse anywhere a KPI can be selected in Feedier: insights, reports, KPI charts, score tables and benchmarks. It is recalculated live, against the filters and the team scope of the page it is displayed on.

Where to create a custom KPI

  1. Go to Settings → Data model → KPI Formulas.

  2. Click Add Formula.

  3. Fill in the Name, an optional Description, and the Formula itself.

  4. Choose a Suffix (%, /5, min, a currency…) and a Format (0, 0.0, 0.00, 0,0, 0,00).

  5. Turn on Lower is better when a lower value is the desired outcome, then set the Visibility of the KPI for your teams.

The formula is executed when you save. If it cannot be computed, Feedier rejects it with “This formula is invalid.”

Best practices when creating KPIs

  1. Use simple, intuitive names that are understood at a glance. Avoid jargon.

  2. Add a short description whenever the scope of the KPI is not obvious from its name. Everyone reading a report should know what the number covers.

  3. Always set the suffix and the format. A KPI with no unit is read wrong: % for percentages, /5 for ratings, min or h for durations, a currency symbol for money. There is no prefix field — the suffix is appended after the value.

  4. Turn on Lower is better for KPIs where a drop is good (detractor rate, churn, response time, customer effort), so trends and colours are read the right way across the platform.

  5. Prefer one clear KPI per question. Two simple formulas are easier to maintain than one formula doing everything.

Understanding the Formula Builder

A custom KPI is a mathematical expression built from one or several Feedier functions. Each function can be narrowed down with conditions.

➡️ A condition is always written as a group of three values:

function("parameter.property", "operator", "value")

Part

Role

Example

Function

The metric you want to measure.

feedback()

Parameter + property

The field you filter on, written with a dot: the parameter, then the property.

"import.type"

Operator

How the value must match. See 3. Operators below.

"="

Value

A single value, or an array of values.

"trustpilot"

Put together:

feedback("import.type", "=", "trustpilot")

⚠️ The double quotes around the parameter, the operator and the value are mandatory. Only booleans and numbers inside an array can be written without quotes.

Several conditions

Repeat the group of three. Conditions are combined with AND, and the total number of arguments must always be a multiple of three.

feedback("survey.id", "=", "1526", "nps.profile", "=", "promoter")

Several values for one condition

Pass an array. Values inside an array are combined with OR.

feedback("team.id", "=", ["1234", "323", "12332"])

No condition at all

A function called with no argument returns the metric over the whole scope currently applied to the page.

feedback()

Every function is always restricted by the filters, the date range and the team scope of the page where the KPI is displayed, on top of the conditions you write. A formula can never return data a user is not allowed to see.

Combining functions

Functions can be combined with +, -, *, /, ^ (power), % (modulo) and parentheses.

feedback("nps.profile", "=", "detractor") / feedback() * 100

A division by zero returns 0 instead of failing. A KPI stuck at 0 is often a denominator that matches nothing.

1. Functions

All the functions available in the formula builder, and what they return.

Function

Returns

Unit / scale

feedback()

Number of feedback matching the conditions.

Count

response()

Number of feedback having at least one matching answer.

Count

push()

Number of feedback invitations sent (email + SMS).

Count

nps()

Net Promoter Score.

−100 to 100

satisfaction()

Satisfaction ratio.

0 to 100

sentiment()

Sentiment score of the analysed text.

0 to 100

ces()

Customer Effort Score.

Scale of the question

ratings()

Average of rating questions.

Scale of the question

ratingsTable()

Average of rating table questions.

Scale of the question

time()

Average time to complete a survey.

Minutes

resolutionTime()

Average time to resolve a feedback.

Seconds

financial()

Estimated revenue impact.

Currency

attribute_avg(), attribute_sum(), attribute_min(), attribute_max(), attribute_qty()

Aggregate of a numeric attribute.

Unit of the attribute

url()

A value read from an external API.

Any

feedback()

Number of feedback matching the conditions.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

feedback()

All feedback in the current scope.

feedback("import.type", "=", "google_review")

Feedback imported from Google reviews.

feedback("team.id", "=", "9921")

feedback("team.id", "=", ["1234", "323", "12332"])

Feedback of one team.

Feedback of several teams.

feedback("segment.id", "=", "1234")

Feedback belonging to a segment.

feedback("survey.id", "=", "1526")

Feedback collected on one source.

feedback("question.type", "=", "nps")

Feedback that contains an NPS question.

feedback("question.id", "=", "1526")

feedback("question.id", "=", ["1526", "1527"])

Feedback that contains question 1526.

Feedback that contains either question.

feedback("question.option_name", "=", "Breakfast")

Feedback where the choice or rating-table option “Breakfast” was answered.

feedback("topic.name", "=", "Shipping cost")

Feedback tagged with the topic “Shipping cost”.

feedback("attribute.name", "=", "city")

feedback("attribute.name", "=", "city:Berlin")

Feedback that carries the attribute city.

Feedback where city equals Berlin.

feedback("nps.profile", "=", "promoter")

Feedback from promoters.

feedback("feedback.type", "=", "completed")

Completed feedback. See the note on feedback.type in section 2.

⚠️ push.canal and push.status are not accepted by feedback(). They only work inside push(). A formula using them in feedback() is rejected as invalid.

response()

Number of feedback having at least one answer that matches the conditions.

Unlike feedback(), the conditions of response() are evaluated at answer level, which is what makes the strict operator == meaningful: several conditions written with == must be satisfied by the same answer.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

response("question.id", "=", "11111")

Feedback in which question 11111 was answered.

response("response.value", "=", [4, 5],
"question.type", "=", "ratings")

Feedback with at least one rating answered 4 or 5.

response("response.value", "==", "5",
"question.option_name", "==", "Breakfast")

Feedback where the row “Breakfast” itself was rated 5 — both conditions on the same answer.

response("feedback.type", "=", "incomplete",
"question.type", "=", "textarea")

Feedback left incomplete in which a text question was answered.

push()

Number of feedback invitations sent through Feedier Surveys (emails and SMS).

Accepts: push and survey only. Only the = operator is supported.

Formula

Result

push()

All invitations sent for the sources in scope.

push("push.canal", "=", "sms")

Invitations sent by SMS.

push("push.status", "=", "bounce")

Invitations that bounced.

push("push.status", "=", ["opened", "clicked"])

Invitations opened or clicked.

push("push.includeFollowUps", "=", true,
"push.status", "=", ["scheduled", "requested"])

All invitations including reminders / follow-ups.

push("survey.id", "=", "1526")

Invitations sent for one source.

push() reads the Feedier Survey sending data. It requires a Feedier Survey connection on your workspace and returns 0 when there is none, or when no Feedier Survey source is in scope. Invitations only exist for feedback collected with Feedier Surveys — never for CSV, API, SFTP or review imports.

nps()

Net Promoter Score = % of promoters − % of detractors, computed on NPS questions only.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

nps()

NPS of the current scope.

nps("survey.id", "=", "11")

NPS of one source.

nps("team.id", "=", "9921")

NPS of one team.

nps("segment.id", "=", "111")

NPS of one segment.

nps("import.type", "=", "csv")

NPS of the feedback imported by CSV.

nps("question.id", "=", "11")

NPS of one specific NPS question. The question must be of type nps.

nps("attribute.name", "=", "country:France")

NPS of the feedback whose country attribute is France.

satisfaction()

Average satisfaction ratio, from 0 to 100.

The satisfaction ratio is computed by Feedier on every feedback, from all the scored items it contains. Text questions excluded from the ratio at import time are not counted.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

satisfaction()

Satisfaction ratio of the current scope.

satisfaction("survey.id", "=", "11")

Satisfaction ratio of one source.

satisfaction("team.id", "=", "9921")

Satisfaction ratio of one team.

satisfaction("topic.name", "=", "Packaging")

Satisfaction ratio of the feedback tagged “Packaging”.

satisfaction("nps.profile", "=", "detractor")

Satisfaction ratio of detractors.

sentiment()

Average sentiment score of the analysed text, from 0 to 100.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

sentiment() is the function where the difference between the standard and the strict operators matters most, because a feedback can contain several verbatims tagged with different topics.

Formula

Result

sentiment("topic.name", "=", "Packaging")

Sentiment of the whole feedback that have the topic “Packaging” attached.

sentiment("topic.name", "==", "Packaging")

Sentiment of the verbatims only that carry the topic “Packaging”.

sentiment("topic.name", "!=", "Packaging")

Sentiment of the feedback that do not have the topic attached.

sentiment("topic.name", "!==", "Packaging")

Sentiment of the feedback, excluding the verbatims tagged “Packaging”.

sentiment("question.name", "==", "What did you find most challenging?")

Sentiment of that specific question only, across all feedback.

ces()

Average Customer Effort Score, computed on CES questions only.

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

ces()

CES of the current scope.

ces("topic.name", "=", "Delivery")

CES of the feedback tagged “Delivery”.

ces("question.id", "=", "1526")

CES of one specific CES question.

ratings()

Average value of rating questions (single rating questions only).

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

ratings()

Average rating of the current scope.

ratings("question.id", "=", "1526")

Average of one rating question.

ratings("attribute.name", "=", "gender:female")

Average rating of the feedback whose gender attribute is female.

ratingsTable()

Average value of rating table questions (a question with several rows to rate).

Accepts: attribute, feedback, import, nps, question, response, segment, survey, team, topic.

Formula

Result

ratingsTable()

Average of all rating tables in scope.

ratingsTable("question.option_name", "=", "Breakfast")

Average of the “Breakfast” row.

ratingsTable("question.type", "=", "nps")

Average of the rating tables, restricted to the feedback that also contain an NPS question.

time()

Average time taken to complete a survey, returned in minutes.

Accepts: feedback, import, nps, question, segment, survey only.

Formula

Result

time()

Average completion time of the current scope, in minutes.

time("survey.id", "=", "1324")

Average completion time of source 1324.

time() * 60

The same value expressed in seconds.

resolutionTime()

Average time between the creation of a feedback and the moment it is marked as resolved, returned in seconds.

Accepts: feedback, import, nps, question, segment, survey only.

Formula

Result

resolutionTime()

Average resolution time, in seconds.

resolutionTime() / 3600

Average resolution time in hours.

resolutionTime() / 86400

Average resolution time in days.

resolutionTime("survey.id", "=", "1324")

Average resolution time for one source.

Only feedback that has been assigned to an owner is taken into account. Unassigned feedback never enters the average. Remember to divide the result and to set the matching suffix (h, d) on the KPI.

financial()

Estimated revenue impact of the feedback in scope.

The value is computed as (opportunities - risks) x customer average value. A feedback is classified as an opportunity or a risk from the sentiment of its verbatims.

Accepts: financial only.

Formula

Result

financial()

Net impact: opportunities minus risks.

financial("financial.type", "=", "risk")

Revenue at risk. The value is negative, since only the risk side is counted.

financial("financial.type", "=", "opportunity")

Revenue opportunity.

This function uses the Customer average value defined in Settings → Teams → Advanced. If that value is not set, financial() returns 0.

attribute_avg(), attribute_sum(), attribute_min(), attribute_max(), attribute_qty()

Aggregate the values of a numeric attribute over the feedback in scope.

These five functions all work the same way and only differ by the operation: average, sum, minimum, maximum, and number of values.

Accepts: aggregate_attribute (required), attribute, feedback, import, nps, response, segment, survey, team, topic. The question parameter is not available here.

Formula

Result

attribute_sum("aggregate_attribute.name", "=", "order_amount")

Total of the order_amount attribute.

attribute_avg("aggregate_attribute.name", "=", "order_amount")

Average order amount.

attribute_avg("aggregate_attribute.name", "=", "order_amount",
"nps.profile", "=", "detractor")

Average order amount of detractors.

attribute_max("aggregate_attribute.id", "=", "1232")

Highest value of attribute 1232.

attribute_qty("aggregate_attribute.name", "=", "order_amount")

Number of feedback carrying a value for that attribute.

aggregate_attribute designates which attribute is aggregated, while attribute is a plain filter. In attribute_avg("aggregate_attribute.name", "=", "order_amount", "attribute.name", "=", "city:Berlin"), Feedier averages order_amount over the feedback coming from Berlin. The attribute must hold numeric values.

url()

Call an external JSON endpoint and use one of its values in your KPI.

Accepts: link only, with the link.link property.

url("link.link", "=", "https://example.com/api/metric~data.value")

Element

Description

The URL

Must start with http:// or https://. It is called server-side with a GET request.

~ path

Everything after the ~ is the dot-notation path of the value to read in the JSON response. When omitted, Feedier reads the response key.

{fql}

Replaced by the filters currently applied to the page, so your endpoint can return a value consistent with the dashboard.

{user.id}, {user.email}

Replaced by the identity of the user viewing the KPI.

{apikey}

Replaced by the private API key of the workspace. Useful to call the Feedier public API itself.

⚠️ The request is sent with your workspace private API key attached, so only point url() at endpoints you control or trust. A slow endpoint makes the KPI time out, and the whole formula fails.

2. Parameters and properties

A parameter is the field you filter on. The property comes after the dot and tells Feedier which form the value takes — an id, a name, a type…

Parameter.property

Accepted values

Notes

attribute.id

attribute.name

An attribute id or name, optionally followed by : and a value.

"city" keeps the feedback carrying that attribute. "city:Berlin" keeps only those where it equals Berlin. Works with 1232:male too.

aggregate_attribute.id

aggregate_attribute.name

An attribute id or name.

Required by the attribute_*() functions. Designates the attribute to aggregate, not a filter.

feedback.id

A feedback id.

—

feedback.type

completed · incomplete

Only applies to Feedier Survey feedback: a feedback imported from another source is neither completed nor incomplete and is excluded when you use this parameter.

financial.type

risk · opportunity

Only for financial().

import.type

The source the feedback comes from: feedier_survey, csv, email, chat, review, document, letter, sms, whatsapp, call_center, facetoface, feedback_box, trustpilot, google_review, google_reviews_competitor, google_play_reviews, apple_app_store_reviews, amazon_reviews, avis_verifies, glassdoor, indeed, homeviews, facebook, linkedin, instagram, tiktok, hubspot, salesforce, zendesk, easiware, bigquery…

The value must exist in your workspace. There is no "all" value: to count everything, call the function with no condition.

link.link

An http or https URL.

Only for url().

nps.profile

promoter · passive · detractor

Promoters score 9–10, passives 7–8, detractors 0–6. The value is passive, not “neutral”.

push.canal

email · sms

Only for push().

push.status

scheduled · requested · processed · delivered · opened · clicked · bounce · dropped · deferred · spamreport · unsubscribe

Only for push(). Note that the bounce status is bounce, not “bounced”.

push.includeFollowUps

true · false

Only for push(). Include the reminders on top of the first invitation.

question.id

question.name

A question id, or its exact label.

The label must match exactly, accents and punctuation included.

question.type

nps · textarea · ratings · ratingsTable · ces · choices · order · smiley

Older sources may still carry the legacy types select, orderedList, dropdown or toggle. Rating questions are ratings — there is no slider type.

question.option_id

question.option_name

The id or the label of a choice, or of a rating table row.

—

response.value

One value, or an array of values: "5", [4, 5].

The answer itself. Combine it with a question.* condition to target a given question.

segment.id

A segment id.

—

survey.id

A source id.

—

team.id

A team id.

—

topic.name

The exact name of a topic.

Topics come from your text analysis taxonomy.

Values are checked against your own workspace when you save. An unknown team id, topic name or question label makes the whole formula invalid, even if the rest is correct. The quickest way to get an id is to open the corresponding page in Feedier and read it in the URL.

3. Operators

The operator sits between the parameter and the value, and decides at which level the condition is evaluated.

Operator

Scope

Description

=

Whole feedback 📦

Keeps the feedback for which the condition is true at feedback level.

!=

Whole feedback 🚫

Excludes the feedback for which the condition is true at feedback level.

==

Feedback item 🎯

Strict. The condition must be satisfied by one single item — an answer, a verbatim, a topic occurrence. Several strict conditions must be met by the same item.

!==

Feedback item 🚫

Strict exclusion. Removes the matching items from the calculation, but keeps the rest of the feedback.

Two rules to remember

  • The strict operators == and !== are only allowed on the question, response and topic parameters. Used anywhere else, the formula is rejected.

  • ===, =! and ==! do not exist. The four operators above are the only valid ones.

The clearest illustration is sentiment() on a topic:

Formula

What is measured

sentiment("topic.name", "=", "Packaging")

The whole feedback that mention packaging — including what they say about everything else.

sentiment("topic.name", "==", "Packaging")

Only the verbatims about packaging.

sentiment("topic.name", "!=", "Packaging")

The feedback that never mention packaging.

sentiment("topic.name", "!==", "Packaging")

All the feedback, minus what they say about packaging.

4. The most used custom KPIs

Response rate

feedback("import.type", "=", "feedier_survey") / push() * 100

Suffix %. Both sides of the division must come from the same source: invitations are only generated when you send a Feedier Survey campaign, so feedback coming from any other import (CSV, API, SFTP, Salesforce, reviews…) cannot contribute to a response rate and is excluded from the numerator.

Example — a source mixing CSV and Feedier Survey:

  • Feedback collected: 238, of which 57 from Feedier Survey and 181 from CSV

  • Invitations sent: 654

  • Response rate = 57 / 654 = 8.7 % — the 181 CSV feedback are excluded

If your scope contains no Feedier Survey feedback, this KPI returns 0. That is expected, not a bug.

Detractor rate

feedback("nps.profile", "=", "detractor")
/ (feedback("nps.profile", "=", "promoter")
+ feedback("nps.profile", "=", "passive")
+ feedback("nps.profile", "=", "detractor")) * 100

Suffix %. Turn on Lower is better for this one.

Promoter rate

feedback("nps.profile", "=", "promoter")
/ (feedback("nps.profile", "=", "promoter")
+ feedback("nps.profile", "=", "passive")
+ feedback("nps.profile", "=", "detractor")) * 100

Satisfaction rate on rating questions (CSAT)

feedback("response.value", "=", [4, 5], "question.type", "=", "ratings")
/ feedback("question.type", "=", "ratings") * 100

Suffix %. Share of the feedback whose rating is 4 or 5, out of those that contain a rating question.

Response rate on a given score

response("response.value", "=", [3, 4, 5], "question.id", "=", "123274")
/ feedback("question.id", "=", "123274") * 100

Share of feedback coming from Google reviews

feedback("import.type", "=", "google_review") / feedback() * 100

The denominator is feedback() with no condition, which covers the whole scope. There is no "all" import type.

Share of respondents who picked a given choice

feedback("question.option_name", "=", "Option A")
/ feedback("question.type", "=", "choices") * 100

Average resolution time in hours

resolutionTime() / 3600

Suffix h, format 0.0, Lower is better enabled.

Revenue at risk

financial("financial.type", "=", "risk")

Suffix: your currency. The value is negative, which is what you want on a “revenue at risk” tile.

Average basket of detractors

attribute_avg("aggregate_attribute.name", "=", "order_amount",
"nps.profile", "=", "detractor")

5. Troubleshooting

Symptom

Cause

Fix

“This formula is invalid.”

The formula is executed when you save. Any error — syntax, unknown value, unsupported parameter — blocks the save.

Check the three points below in order: syntax, values, supported parameters.

Syntax error

A missing quote, a missing comma, unbalanced parentheses, or a number of arguments that is not a multiple of three.

Every condition is exactly three arguments: "parameter.property", "operator", "value".

Unknown value

A team id, a topic name, a question label or an import type that does not exist in your workspace.

Copy the exact label, accents included, or take the id from the URL of the corresponding page in Feedier.

Unsupported parameter

The parameter is not accepted by that function — for example push.canal inside feedback(), or team.id inside time().

Check the Accepts line of the function in section 1.

Unsupported operator

== or !== used on a parameter other than question, response or topic. Or an operator other than = inside push().

Fall back to = or !=.

The KPI shows 0

A division by zero returns 0. The scope may also simply contain no matching feedback.

Run the numerator and the denominator as two separate KPIs to find which one is empty.

push() returns 0

No Feedier Survey connection on the workspace, or no Feedier Survey source in the current scope.

Invitations only exist for Feedier Surveys. Check the source of the feedback you are measuring.

url() fails

The endpoint is too slow and the call times out, or the path after the ~ does not exist in the JSON answer.

Test the URL outside of Feedier, and check the exact key path of the value.

The value looks off by a factor

A unit was not converted: resolutionTime() is in seconds, time() is in minutes.

Divide the result and set the matching suffix on the KPI.

Did this answer your question?