Skip to content

Linked Interviews

In real-world document workflows, one interview often leads to another. A triage questionnaire might determine which of several agreement types are appropriate. A master services agreement naturally leads to a data processing agreement, an SLA, or a statement of work. VIBE's linked interviews let you connect these interviews while keeping each one fully independent.

What Can You Do With Linked Interviews?

  • Triage routing: A short interview determines which of several follow-up interviews are relevant, then offers "Continue" links to each
  • Satellite interviews: A primary interview (e.g. MSA) recommends related follow-up interviews (DPA, SLA, SOW) that share common data
  • Data carry-forward: Shared data (party names, contract details) flows from one interview to the next without re-asking the same questions
  • Full independence: Every template works on its own — links are optional connections, never hard dependencies

How It Relates to Other VIBE Features

If you're familiar with other VIBE features, linked interviews will feel natural because they build on the same patterns:

Feature What it does Where the result goes
insert() Pulls a component into the current document Same document, inline
appendix() Creates a separate document from a component Separate file, same interview
recommend() Links to a separate interview with its own template Separate interview session, separate document

Think of it as a progression of scope:

  • insert() — reuse within a document
  • appendix() — separate document, same interview
  • recommend() — separate interview, separate document

All three functions follow the same calling convention: a positional identifier followed by optional keyword arguments for data mapping.

The _linked Namespace

When a user follows a recommend() link, the source interview's complete answered state becomes available in the target template under _linked.<source_template_id>. This is analogous to how _components provides access to component data.

{# In beredskapsavtal, after following a link from verktygsvaljaren #}
{% if _linked.verktygsvaljaren is defined %}
  Leveransomfattning: {{ _linked.verktygsvaljaren.leveransomfattning }}
{% endif %}

Key properties:

  • _linked always exists — it is seeded as an empty container on every session, so {% if _linked.X is defined %} never causes an undefined error.
  • _linked.<template_id> only exists when linked — the subtree is only present when the user followed a recommend() link from that template.
  • Read-only — linked values cannot be modified by the user in the target interview. This ensures coherence across the document package.
  • No declaration needed — unlike uses: for session context, _linked does not require any configuration in the target template's config.yml. Any template can read _linked if data happens to be there.

Checking for Linked Data

Use the Jinja is defined test to check for container nodes (whether a source template is linked at all), and the defined() function to check for specific scalar values:

{# Check if we were linked from the MSA interview #}
{% if _linked.msa is defined %}
  Client: {{ _linked.msa.party_info.client_name }}
{% endif %}

{# Check if a specific field has a value #}
{% if defined("_linked.msa.party_info.client_name") %}
  Client: {{ _linked.msa.party_info.client_name }}
{% endif %}

The is defined test checks whether a key exists in the template context (including container nodes). The defined() function checks whether a specific path has a non-None scalar value in the session state.

What Gets Linked

When the user follows a recommendation link, the entire answered state of the source interview is copied under _linked.<template_id>. Internal keys (_components, _appendices, _linked) are filtered out — only the template's own questions and session context values are included.

Your First Linked Interview

Let's build a simple example: a main contract interview that recommends a follow-up NDA interview when confidentiality is needed.

Step 1: Create the Main Interview

The main template collects party info and recommends the NDA when appropriate.

# contract/config.yml
title: Service Agreement

questions:
  client_name:
    type: text
    label: Client name
  client_org_nr:
    type: text
    label: Organization number
  service_description:
    type: text
    label: Describe the services
  confidentiality_needed:
    type: bool
    label: Does this engagement involve confidential information?
<!-- contract/template.md -->

# Service Agreement

This agreement is between our company and {{ client_name }}
(org. nr {{ client_org_nr }}).

## Services
{{ service_description }}

## Confidentiality
{% if confidentiality_needed %}
The parties acknowledge that confidential information may be exchanged.
A separate Non-Disclosure Agreement shall be executed.

{{ recommend('nda', "Non-Disclosure Agreement") }}
{% endif %}

Step 2: Create the Follow-Up Interview

The NDA template is a fully independent interview. It uses _linked to access data from the contract interview when available, and falls back to asking its own questions when launched standalone:

# nda/config.yml
title: Non-Disclosure Agreement

questions:
  client_name:
    type: text
    label: Client name
  confidentiality_period:
    type: number
    label: Confidentiality period (years)
  covers_employees:
    type: bool
    label: Does the NDA cover employees of both parties?
<!-- nda/template.md -->

# Non-Disclosure Agreement

{% if _linked.contract is defined %}
{# Linked from the contract — use the contract's client name #}
This NDA is between our company and {{ _linked.contract.client_name }}.
{% else %}
{# Standalone — use our own question #}
This NDA is between our company and {{ client_name }}.
{% endif %}

The confidentiality obligations shall remain in effect for
{{ confidentiality_period }} years.

{% if covers_employees %}
The obligations extend to all employees and contractors of both parties.
{% endif %}

What Happens

NDA accessed directly (standalone mode):

  1. User navigates to the NDA interview
  2. _linked.contract is not defined (no link was followed)
  3. User fills in client name plus NDA-specific questions
  4. Document is generated

NDA accessed from the Service Agreement (linked mode):

  1. User completes the Service Agreement interview
  2. Completion page shows: "Non-Disclosure Agreement [Continue →]"
  3. User clicks "Continue"
  4. A new NDA interview starts with the contract's full state available under _linked.contract
  5. The template's {% if _linked.contract is defined %} branch activates, using the contract's client name
  6. Only NDA-specific questions are asked (confidentiality period, etc.)
  7. Document is generated

The NDA template works in both cases. The only difference is whether _linked.contract is available.

The recommend() Function

{{ recommend('template_id') }}
{{ recommend('template_id', "Display label") }}
{{ recommend('template_id', "Display label", target_name=source_value) }}

Parameters:

Parameter Position Required Description
template_id 1st positional Yes The identifier of the target template
label 2nd positional No Display name shown on the completion page. Falls back to the target template's title from config.yml
**kwargs Keyword No Explicit name mappings for uses:-based session context when names differ

This follows the same calling convention as insert(): positional arguments identify the target, keyword arguments map data.

Behavior:

  • recommend() renders as empty string in the document — it produces no visible output. The recommendation text around it is normal template content that you write yourself.
  • Recommendations are discovered by probing, just like insert() and appendix(). If a recommend() call is inside an {% if %} block that doesn't apply, it's never reached and the recommendation isn't offered.
  • Multiple recommend() calls can be active at the same time (e.g. the MSA recommending both a DPA and an SLA).
  • recommend() targets are validated when the template is loaded. If a target template ID does not exist, validation fails early.

How Data Flows Between Interviews

When a user clicks "Continue" on a recommendation link:

  1. The source interview's complete answered state is serialized
  2. Internal keys (_components, _appendices, _linked) are filtered out
  3. The remaining state is placed under _linked.<source_template_id> in the target interview's session state
  4. The target template accesses it via {{ _linked.<source_template_id>.field }}

No configuration is needed on either side for this to work. The source template calls recommend('target'), and the target template reads _linked.source — that's it.

Explicit Mappings via uses: Declarations

When the target template has uses: declarations (with definition-only components), recommend() keyword arguments can map source values to the target's uses: aliases. This uses the same mechanism as session context validation but is independent of _linked — the mapped data arrives under the alias name, not under _linked:

{# Source calls it kund_info, target's uses: expects party_info #}
{{ recommend('dpa', party_info=kund_info) }}

No Data Forwarding

If recommend() is called without keyword arguments, the _linked data still flows automatically. The target template decides what to use from it. For triage scenarios where the triage asks routing questions but the follow-up interviews collect their own data independently, _linked is simply ignored:

{# Pure routing — target template doesn't reference _linked #}
{{ recommend('ersättningslöst') }}

Linked Values Are Read-Only

Linked values are read-only in the target interview. This ensures coherence across the document package — if the MSA says the client is "Acme Corp", the DPA must agree.

This parallels how session context works: values from external systems are visible in the template but cannot be changed by the user during the interview.

Relationship to Session Context and uses:

The uses: mechanism in config.yml serves a different purpose from _linked. Here is how they compare:

Mechanism Data source Configuration Editable?
Session context (uses:, required) External system (auth token) config.yml uses: No
uses: mapping via recommend() A completed VIBE interview config.yml uses: No
_linked A completed VIBE interview None needed No
User answers The user during the interview config.yml questions Yes

uses: with definition-only components validates that structured data has the right shape. The data can come from external systems (PASETO tokens) or from another interview via recommend() kwargs — the mechanism is the same, but the data arrives with different ValueSource tags internally.

_linked provides raw access to another interview's full answered state. No declarations are needed — the target template reads what's there, guarded by {% if _linked.X is defined %}.

Both can coexist. A template can use uses: declarations for validated structured data AND read _linked for ad-hoc access to the source interview's answers.

Example: Triage Interview

A triage interview asks a few high-level questions and then routes the user to the appropriate follow-up:

# triage/config.yml
title: Agreement Selector

questions:
  myndighet_namn:
    type: text
    label: Myndighetens namn
  reason:
    type: enum
    multiple: true
    label: Why does your organization need this agreement?
    options:
      samverkan: Establish joint planning
      investering: Specific investments needed
      leverans: Secure delivery of goods/services
  # ... more triage questions
<!-- triage/template.md -->

{{ reason }}

## Our Recommendation

{% if 'samverkan' in reason %}
Based on your answers, we recommend a non-compensatory preparedness agreement.

{{ recommend('ersättningslöst', "Ersättningslöst beredskapsavtal") }}
{% endif %}

{% if 'leverans' in reason %}
We recommend preparedness conditions for public procurement.

{{ recommend('beredskapsavtal', "Beredskapsavtal") }}
{% endif %}

Each follow-up template can read the triage answers via _linked.triage:

<!-- beredskapsavtal/beredskapsavtal.md -->

{% if _linked.triage is defined %}
Myndighet: {{ _linked.triage.myndighet_namn }}
{% endif %}

After completing the triage, the user sees only the relevant follow-up links. Multiple recommendations can be active simultaneously.

Example: Primary + Satellite Interviews

An IT sourcing MSA that leads to optional DPA, SOW, and SLA interviews:

# msa/config.yml
title: Master Services Agreement

questions:
  client_name:
    type: text
    label: Client name
  client_org_nr:
    type: text
    label: Organization number
  data_processing:
    type: bool
    label: Does this engagement involve processing personal data?
  service_description:
    type: text
    label: Describe the services
<!-- msa/template.md -->

# Master Services Agreement

This MSA is between {{ client_name }} and our company.

## Services
{{ service_description }}

## Related Agreements

{% if data_processing %}
Personal data will be processed under this agreement. A separate Data
Processing Agreement is required.

{{ recommend('dpa', "Data Processing Agreement") }}
{% endif %}

{{ recommend('sla', "Service Level Agreement") }}

Each follow-up template reads shared data from _linked.msa:

<!-- dpa/template.md -->

# Data Processing Agreement

{% if _linked.msa is defined %}
This DPA supplements the Master Services Agreement between
{{ _linked.msa.client_name }} (org. nr {{ _linked.msa.client_org_nr }})
and our company.
{% else %}
{# Standalone mode — ask our own questions #}
This DPA is between {{ client_name }} and our company.
{% endif %}

Each follow-up template works independently. When accessed from the MSA, _linked.msa provides the shared data. When accessed standalone, the template falls back to its own questions.

Passing _linked Data to Components

The _linked namespace is NOT automatically passed through to components (it starts with _, following the same convention as _components). If a component needs linked data, pass it explicitly via insert() kwargs or component_defaults:

{# Pass specific linked values to a component #}
{{ insert('signature_block',
    signer_name=_linked.msa.client_name) }}
# Or via component_defaults in config.yml
component_defaults:
  client_name: "{{ _linked.msa.client_name if _linked.msa is defined else client_name }}"

How Recommendations Appear in the UI

During the interview

As you answer questions and probing discovers active recommend() calls, links to the recommended follow-up interviews appear below the main question list. These links are not shown until the relevant recommend() call is actually reached by probing — if it's inside an {% if %} block that hasn't been triggered yet, there's nothing to show.

On the completion page

When the interview completes, the completion page shows the document download and the recommended follow-ups:

[x] Master Services Agreement is ready.      [Download DOCX]

━━━ Recommended follow-ups ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
● Data Processing Agreement                 [Continue →]
● Service Level Agreement                   [Continue →]

Only active recommendations appear — those inside {% if %} blocks that evaluated to true. The user chooses which follow-ups to pursue. Each "Continue" link starts a fresh interview session for the target template.

When an interview is launched from a linked recommendation, it knows where it came from. This enables optional "← Back to Master Services Agreement" navigation in the UI. The back-link is purely informational — the target template has no functional dependency on the source.

When the same template is launched standalone, there is no back-link.

Key Differences from Components and Appendices

insert() appendix() recommend()
Target A component A component A template
Output Content in your document Separate file, same package Separate document from a separate interview
Questions Injected into current interview Injected into current interview Asked in a new interview session
Data sharing Via keyword arguments Via keyword arguments Automatic via _linked, or keyword args for uses: mappings
User experience Seamless — part of one interview Seamless — part of one interview A new interview starts after the current one completes
Independence Component can't run alone Component can't run alone Target template is fully independent
Authorship Usually same author Usually same author Can be different teams

Tips and Best Practices

Guard with {% if _linked.X is defined %}. Always check before accessing linked data. The template must work standalone too.

Keep triage templates focused. A triage interview should ask only the questions needed to determine which follow-ups are relevant. Leave detailed questions to the follow-up templates.

Write the recommendation text yourself. recommend() renders nothing visible — it just registers the link. Write the explanatory text around it as normal template content. This gives you full control over the wording.

Pass linked data to components explicitly. Use insert() kwargs or component_defaults to forward _linked values to components that need them.

Test templates both standalone and linked. Since every template must work independently, test each follow-up template on its own first. Then test the link to verify that _linked data arrives correctly.

Multiple recommendations can be active. It's perfectly valid for a triage to recommend two or three follow-ups simultaneously. The user sees all of them on the completion page and can pursue any or all.