In-app surveys
Fire an event from this page, answer the NPS card it drops in your feed, and watch a second journey read your score back off the spine — one identity, no login.
This is the same in-app feed surface the nav bell renders — here it's inlined on the page so you can answer a survey without leaving. You are an anonymous visitor: no login, no identify call. Fire the event, answer the card, and the answer routes back through a journey.
Fires demo.survey. The NPS card lands in the feed below — answer it and a journey drops the thank-you item.
import { days } from "@hogsend/core";
import { defineJourney, sendFeedItem, sendSurvey } from "@hogsend/engine";
import { Events } from "./constants/index.js";
// demo.survey → drop an in-app NPS card the visitor answers in their bell.
export const demoSurvey = defineJourney({
meta: {
id: "demo-survey",
name: "Demo — In-app survey",
enabled: true,
trigger: { event: Events.DEMO_SURVEY }, // "demo.survey"
entryLimit: "unlimited", // re-fire freely
suppress: days(0),
},
run: async (user) => {
await sendSurvey({
recipient: { anonymousId: user.id }, // your canonical key
event: Events.DEMO_NPS_SUBMITTED, // "demo.nps_submitted"
mode: "nps",
property: "score", // the answer rides here
prompt: "How likely are you to recommend Hogsend?",
title: "Quick question 👇",
minLabel: "Not likely",
maxLabel: "Very likely",
});
// sendSurvey has NO journeyStateId option — replay-safety is auto-keyed off
// the Hatchet run anchor inside sendFeedItem.
},
});
// demo.nps_submitted → a thank-you item echoing the score (closes the loop).
export const demoNpsAnswered = defineJourney({
meta: {
id: "demo-nps-answered",
name: "Demo — NPS answered → thank-you",
enabled: true,
trigger: { event: Events.DEMO_NPS_SUBMITTED }, // "demo.nps_submitted"
entryLimit: "unlimited",
suppress: days(0),
},
run: async (user) => {
const raw = user.properties.score; // SurveyBlockView captured it under "score"
const score =
typeof raw === "number" || typeof raw === "string" ? String(raw) : "?";
await sendFeedItem({
recipient: { anonymousId: user.id },
type: "survey-thanks",
title: `Thanks — you scored ${score} 🙏`,
body: "You answered an in-app survey. That emitted demo.nps_submitted onto the spine — a journey read your score and dropped this.",
actionUrl: "https://hogsend.com/docs/client-side/survey",
journeyStateId: user.stateId, // sendFeedItem DOES accept this
});
},
});What just happened
The button called client.capture("demo.survey", {}) — a first-party event keyed to your anonymous id.
A journey triggered on demo.survey and called sendSurvey({ mode: "nps", property: "score", event: "demo.nps_submitted" }). That published a feed item carrying a survey block, which FeedItemView auto-renders as an NPS card via SurveyBlockView.
Answering the card captured demo.nps_submitted with your score under the score property (SurveyBlockView writes { [property]: value, feedItemId, source: "in_app" } and reuses an idempotencyKey so a re-answer is exactly-once). It lands in user_events.properties like any other first-party event.
A second journey triggered on demo.nps_submitted, read user.properties.score, and called sendFeedItem — the thank-you item that echoes your number. Same write-back path a real NPS or CSAT loop uses.
The aggregate is queryable off the same events:
GET /v1/admin/reporting/breakdown?event=demo.nps_submitted&property=score&metric=npsThe whole round-trip used one identity. A pk_ publishable key has no user token, so userId stays null and the feed stays keyed to your anonymous id end to end.
Try it live
Fire a first-party event from this page and watch a journey drop a notification into the bell in the top nav — same anonymous identity, no login.
Preferences
A live <PreferenceCenter> read of the dogfood engine's list catalog — each code-defined list at its defaultOptIn state, over GET /v1/lists.