When launching a new product or after changes in a service, the same picture often emerges: managers manually collect feedback via email or messengers, data gets lost, analysis lags behind. Recently, a client from an electronics online store came to us — their Net Promoter Score was unknown because surveys were conducted once every six months via email with a 3% response rate. We placed a pop-up survey on the order page, and within a month we received 1200 responses with a detailed picture. In one month, NPS increased from 20 to 45 — this allowed us to quickly fix problematic areas in the delivery service. A survey on the site is the only way to get structured data without losses. Bitrix does not have a ready-made module for surveys of a decent level — the built-in iblock can be used for storage, but the survey logic has to be written from scratch.
Implementation of a survey on 1C-Bitrix
We build the architecture on HL-blocks — this is faster and more flexible than infoblocks. Typical scenarios:
- NPS (Net Promoter Score) — 0–10 scale with comment.
- CSAT (Customer Satisfaction) — 1–2 questions after a specific action.
- Full survey — several pages with different question types, branching, display triggers.
Data structure via HL-blocks:
class SurveyTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_surveys'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new StringField('TITLE'),
new StringField('SLUG'),
new StringField('TYPE'), // nps | csat | full
new TextField('QUESTIONS_JSON'), // Question configuration
new StringField('TRIGGER'), // page_load | exit_intent | scroll_50 | after_order
new IntegerField('SHOW_DELAY'), // Display delay in seconds
new BooleanField('IS_ACTIVE', ['values' => [false, true]]),
new DatetimeField('DATE_FROM'),
new DatetimeField('DATE_TO'),
];
}
}
class SurveyResponseTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_survey_responses'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('SURVEY_ID'),
new IntegerField('USER_ID'), // NULL — anonymous
new StringField('SESSION_ID'),
new StringField('USER_IP'),
new TextField('ANSWERS_JSON'), // {question_id: answer}
new IntegerField('COMPLETION_SEC'), // Completion time
new DatetimeField('CREATED_AT'),
];
}
}
Survey types to implement
Choice depends on the business task. Compare options:
| Type | Essence | Questions | Trigger | Implementation time |
|---|---|---|---|---|
| NPS | Willingness to recommend | 1–2 | Pop-up on delay / exit | 2–3 days |
| CSAT | Specific point evaluation | 1–3 | After order / inquiry | 2–4 days |
| Full | Audience research | 5–20 | Combined | 8–14 days |
NPS survey — the simplest and cheapest way to start collecting feedback. It requires only one component with a widget.
NPS survey: simple case
Typical implementation — one page, one action. Component code:
// /local/components/local/survey.nps/template.php
?>
<div class="nps-widget" id="nps-widget" style="display:none;">
<div class="nps-container">
<button class="nps-close" onclick="NPS.dismiss()">×</button>
<p class="nps-question">
How likely are you to recommend us to your friends?
</p>
<div class="nps-scale">
<?php for ($i = 0; $i <= 10; $i++): ?>
<button class="nps-score" data-score="<?= $i ?>"><?= $i ?></button>
<?php endfor; ?>
</div>
<div class="nps-labels">
<span>Not at all likely</span>
<span>Extremely likely</span>
</div>
<div class="nps-comment" style="display:none;">
<textarea placeholder="Tell us more..." id="nps-comment-text"></textarea>
<button onclick="NPS.submit()">Submit</button>
</div>
</div>
</div>
<script>
const NPS = {
surveyId: <?= (int)$arResult['SURVEY']['ID'] ?>,
selectedScore: null,
init() {
setTimeout(() => {
if (!this.wasShown()) {
document.getElementById('nps-widget').style.display = 'flex';
this.markShown();
}
}, <?= (int)$arResult['SURVEY']['SHOW_DELAY'] * 1000 ?>);
document.querySelectorAll('.nps-score').forEach(btn => {
btn.addEventListener('click', e => {
this.selectScore(parseInt(e.target.dataset.score));
});
});
},
selectScore(score) {
this.selectedScore = score;
document.querySelectorAll('.nps-score').forEach(b => b.classList.remove('selected'));
document.querySelector(`[data-score="${score}"]`).classList.add('selected');
document.querySelector('.nps-comment').style.display = 'block';
},
async submit() {
if (this.selectedScore === null) return;
await fetch('/local/ajax/survey_submit.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
survey_id: this.surveyId,
answers: {
nps_score: this.selectedScore,
comment: document.getElementById('nps-comment-text').value,
},
sessid: BX.bitrix_sessid(),
}),
});
document.getElementById('nps-widget').innerHTML =
'<p class="nps-thanks">Thank you for your feedback!</p>';
setTimeout(() => document.getElementById('nps-widget').style.display = 'none', 3000);
},
dismiss() {
document.getElementById('nps-widget').style.display = 'none';
this.markShown();
},
wasShown() {
return !!localStorage.getItem('nps_shown_' + this.surveyId);
},
markShown() {
localStorage.setItem('nps_shown_' + this.surveyId, Date.now());
},
};
NPS.init();
</script>
Display triggers
The survey should appear at the right moment, not immediately on load. Implement several triggers:
const SurveyTriggers = {
exitIntent(callback) {
document.addEventListener('mouseleave', e => {
if (e.clientY <= 0) callback();
}, {once: true});
},
scrollDepth(percent, callback) {
const listener = () => {
const scrolled = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
if (scrolled >= percent) {
window.removeEventListener('scroll', listener);
callback();
}
};
window.addEventListener('scroll', listener);
},
afterOrder(callback) {
if (window.location.href.includes('/order/success/')) {
setTimeout(callback, 2000);
}
},
};
Triggers are configured individually for each survey and can be combined. For example, a post-order survey Bitrix appears after a successful purchase, while an exit intent survey Bitrix triggers when leaving the page. To prevent duplicate responses, we use localStorage/cookie for anonymous users and check by user_id for authorized ones — this eliminates duplicates.
Analyzing responses
For NPS, automatically calculate the category of promoters, passives, and detractors:
class NpsAnalytics
{
public static function calculate(int $surveyId): array
{
$responses = SurveyResponseTable::getList([
'filter' => ['SURVEY_ID' => $surveyId],
'select' => ['ANSWERS_JSON'],
])->fetchAll();
$detractors = 0; // low scores
$passives = 0; // medium scores
$promoters = 0; // high scores
$total = 0;
foreach ($responses as $r) {
$answers = json_decode($r['ANSWERS_JSON'], true);
$score = (int)($answers['nps_score'] ?? -1);
if ($score < 0) continue;
$total++;
if ($score <= 6) $detractors++;
elseif ($score <= 8) $passives++;
else $promoters++;
}
if ($total === 0) return ['nps' => 0, 'total' => 0];
$nps = round(($promoters / $total - $detractors / $total) * 100);
return [
'nps' => $nps,
'total' => $total,
'promoters' => $promoters,
'passives' => $passives,
'detractors' => $detractors,
];
}
}
Step-by-step survey development process
- Analysis and agreement — define goals (NPS, CSAT, research), choose triggers and question types.
- HL-block design — create tables via migration API, configure fields.
- Component implementation — write widget templates, set up AJAX handlers.
- Trigger integration — add exit-intent, scroll, post-order events.
- Testing and debugging — test on different browsers, resolve conflicts.
- Launch and analytics — deliver dashboard with NPS, CSAT, conversion metrics.
Throughout this survey development Bitrix process, we ensure smooth Bitrix feedback collection.
What's included in the work
When you order a turnkey survey development, we provide:
- HL-block setup and migrations
- Component development (NPS, CSAT, custom)
- Trigger implementation and duplicate protection
- Analytics integration (NPS counter, summary reports)
- API and configuration documentation
- Access to repository and test environment
- 30 days of technical support after launch
Why HL-blocks are better than infoblocks?
Highload-blocks are tables with an arbitrary set of fields. Infoblocks are designed for content filling, while HL-blocks are for fast queries with minimal load. For surveys, write speed without extra events is critical. Comparison table:
| Criterion | Infoblocks | HL-blocks |
|---|---|---|
| Write performance | Medium (events, overhead) | High (direct INSERT) |
| Field flexibility | Fixed types | Arbitrary fields via ORM |
| JSON field support | No | Yes, TextField |
| Suitable for surveys | Only result storage | Configuration and response storage |
Timelines and conditions
Timelines depend on complexity: simple widget — from 2 days, full survey — up to 14 days. Cost is calculated individually, but typical prices start from $500 for a simple NPS widget and up to $2000 for a full survey with branching and multiple triggers. Payback occurs within the first month due to automated data collection and timely problem identification. We rely on HL-blocks — a standard Bitrix tool that guarantees compatibility with updates. Our experience of 10+ years and more than 50 implemented projects confirms the stability of solutions. Get a consultation for your task — we will estimate the scope without rigid deadlines. Order survey development — and within a month you will see the clean NPS of your business.







