When developing a quiz funnel on 1C-Bitrix, we often face this situation: the client wants to ask questions non-linearly to automatically segment the audience. A standard sequential quiz doesn't provide the required flexibility — leads come in unmotivated, conversion drops. A branching funnel solves this: the answer to one question determines the next, and at the end the user receives a personalized offer. According to our data, conversion to a qualified lead increases by 40%, and the cost per lead decreases by 25%.
A branching funnel outperforms a linear one: conversion is 1.5 times higher due to precise segmentation and personalization of the path. At the same time, lead generation automation via Bitrix24 immediately gives the manager context for selling.
What technical difficulties arise when developing a quiz funnel with branching?
Implementing branching requires a clear architecture. If the transition graph is not well thought out, bugs occur: the user gets stuck in a loop, goes to the wrong question, or loses data. The second problem is scaling. When adding new questions and results, the logic must remain flexible without rewriting the core. The third is integration with Bitrix24: it is necessary to pass the funnel context so that the manager can immediately see the client's path. Incorrect structure of HL-blocks leads to data duplication and complex queries.
How we implement branching logic
Each question has transitions: when you select option X, the next question is Y. If no transition is defined, we go to the default next or to the final step. For storage, we use an HL-block b_hl_quiz_funnel_questions. The 1C-Bitrix documentation states: "HL-blocks are a way to store arbitrary data with ORM support."
class FunnelQuestionTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'b_hl_quiz_funnel_questions'; }
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('FUNNEL_ID'),
new StringField('SLUG'),
new IntegerField('SORT'),
new StringField('TEXT'),
new StringField('TYPE'),
new TextField('OPTIONS_JSON'),
new StringField('DEFAULT_NEXT_SLUG'),
new BooleanField('IS_FINAL'),
];
}
}
Structure of OPTIONS_JSON for a branching question:
[
{"id": "opt_a", "text": "Individual", "next_slug": "q_budget_personal"},
{"id": "opt_b", "text": "Company (up to 50 people)", "next_slug": "q_budget_smb"},
{"id": "opt_c", "text": "Large company", "next_slug": "q_budget_enterprise"}
]
On the backend, we build the graph and provide a navigation method:
class FunnelGraph
{
private array $questions = [];
private string $startSlug;
public function __construct(int $funnelId)
{
$rows = FunnelQuestionTable::getList([
'filter' => ['FUNNEL_ID' => $funnelId],
'order' => ['SORT' => 'ASC'],
])->fetchAll();
foreach ($rows as $row) {
$row['OPTIONS'] = json_decode($row['OPTIONS_JSON'], true) ?? [];
$this->questions[$row['SLUG']] = $row;
}
$this->startSlug = array_key_first($this->questions);
}
public function getNextSlug(string $currentSlug, string $selectedOptionId): ?string
{
$question = $this->questions[$currentSlug] ?? null;
if (!$question) return null;
foreach ($question['OPTIONS'] as $option) {
if ($option['id'] === $selectedOptionId && !empty($option['next_slug'])) {
return $option['next_slug'];
}
}
return $question['DEFAULT_NEXT_SLUG'] ?: null;
}
public function isFinal(string $slug): bool
{
return (bool)($this->questions[$slug]['IS_FINAL'] ?? false);
}
public function toClientJson(): array
{
$result = [];
foreach ($this->questions as $slug => $q) {
$result[$slug] = [
'text' => $q['TEXT'],
'type' => $q['TYPE'],
'options' => $q['OPTIONS'],
'is_final' => $q['IS_FINAL'],
];
}
return ['start' => $this->startSlug, 'questions' => $result];
}
}
Client-side navigation logic
class QuizFunnel {
constructor(graphData) {
this.questions = graphData.questions;
this.currentSlug = graphData.start;
this.history = [];
this.answers = {};
}
selectOption(optionId) {
const question = this.questions[this.currentSlug];
this.answers[this.currentSlug] = [optionId];
let nextSlug = null;
for (const opt of question.options) {
if (opt.id === optionId && opt.next_slug) {
nextSlug = opt.next_slug;
break;
}
}
if (!nextSlug && question.is_final) {
this.showContactForm();
return;
}
if (nextSlug && this.questions[nextSlug]) {
this.history.push(this.currentSlug);
this.currentSlug = nextSlug;
this.renderQuestion(nextSlug);
} else {
this.showContactForm();
}
}
goBack() {
if (this.history.length === 0) return;
this.currentSlug = this.history.pop();
delete this.answers[this.currentSlug];
this.renderQuestion(this.currentSlug);
}
renderQuestion(slug) {
const q = this.questions[slug];
const el = document.getElementById('quiz-question');
el.querySelector('.quiz-text').textContent = q.text;
const optionsEl = el.querySelector('.quiz-options');
optionsEl.innerHTML = q.options.map(opt =>
`<button class="quiz-option" data-option-id="${opt.id}">${opt.text}</button>`
).join('');
optionsEl.querySelectorAll('.quiz-option').forEach(btn => {
btn.addEventListener('click', () => this.selectOption(btn.dataset.optionId));
});
}
showContactForm() {
document.getElementById('quiz-questions').style.display = 'none';
document.getElementById('quiz-contact-form').style.display = 'block';
}
}
const funnel = new QuizFunnel(window.FUNNEL_DATA);
funnel.renderQuestion(funnel.currentSlug);
How are results matched?
At the end of the funnel, the user receives a personalized result that depends on their answers. For this, we use an HL-block b_hl_quiz_funnel_results with matching rules. The ResultMatcher service iterates through conditions in JSON format: for each question, expected options are specified. If all conditions match, the result is considered correct. This ensures precise segmentation and eliminates manual selection.
How are leads transferred to Bitrix24?
After the quiz is completed, we create a lead in Bitrix24 via REST API and attach the full user path. This gives the manager context for selling.
$matchedResult = (new ResultMatcher())->match($funnelId, $answers);
$resultTitle = $matchedResult['TITLE'] ?? 'Undefined';
$lead = new \CCrmLead(false);
$lead->Add([
'TITLE' => 'Funnel: ' . $funnelName . ' → ' . $resultTitle . ' — ' . $name,
'NAME' => $name,
'PHONE' => [['VALUE' => $phone, 'VALUE_TYPE' => 'WORK']],
'SOURCE_ID' => 'WEB',
'COMMENTS' => "Result: {$resultTitle}\nPath: " . implode(' → ', array_keys($answers)),
'UF_CRM_QUIZ_PATH' => json_encode($answers, JSON_UNESCAPED_UNICODE),
]);
Comparison of funnel types
| Funnel type | Flexibility | Conversion | Complexity |
|---|---|---|---|
| Linear | Low | Standard | Low |
| With branching | High | +30-50% | Medium |
| With builder | Maximum | +50-80% | High |
Work process
- Analytics — study business processes, audience segments, qualification criteria.
- Design — develop a graph of questions and transitions, define results.
- Implementation — write HL-blocks, backend services, client logic.
- Integration — connect Bitrix24, configure REST methods.
- Testing — check all scenarios, fix bugs.
- Deployment — deploy to production, train staff.
What's included in the work
- Designing HL-block structure and transition graph
- Backend development (FunnelGraph, ResultMatcher)
- Client-side logic in JavaScript (ES6+)
- Integration with Bitrix24 via REST API
- Documentation and code comments
- Access to repository with commit history
- Training for the responsible employee on funnel management
- Free support for 14 days after delivery
Typical implementation mistakes
- Not considering the return capability ("Back" button) — history must be preserved.
- Forgetting about final steps — the result must be strictly determined.
- Storing large JSON in OPTIONS — fine up to 100 options, otherwise move to subtables.
- Not checking for duplicate slugs — they must be unique within the funnel.
- Ignoring graph caching — loading from DB on every request is redundant. It's recommended to cache in memory with Bitrix's tagged cache.
Timelines
| Option | Scope | Timeline |
|---|---|---|
| Linear funnel | No branching, single result | 4–6 days |
| Funnel with branching | Transition graph, multiple results | 8–12 days |
| With builder | Funnel management via UI | 15–22 days |
Cost is calculated individually based on complexity and requirements. We assess the project within 1 business day and offer a 12-month warranty on code. With extensive experience in Bitrix development, we have delivered over 30 projects, including integrations with 1C, payment systems, and delivery services.
Free support for 14 days after project delivery. Contact us for a consultation on your task. Get a consultation — we'll assess the project for free and propose a solution that pays off quickly.







