Needs

The [needs] section is the heart of your ubproject.toml. It tells ubCode how to identify, validate, and relate the need objects in your reStructuredText files — types, link types, custom fields, statuses, tags, and external needs.

See also

Key concepts

Minimal example

[needs]
id_required = true
id_regex = "^[A-Z]{3}_[0-9]{3}$"

[[needs.types]]
directive = "requirement"
title = "Requirement"
prefix = "REQ_"

[needs.links.implements]
outgoing = "implements"
incoming = "implemented by"

[needs.fields.priority]
description = "Priority level"

Core options

id_required

Type: boolean (default: false)

If true, every need must have an explicit ID. If false, IDs will be auto-generated when not provided.

id_regex

Type: string (default: "^[A-Z0-9_]{5,}")

Regular expression pattern to validate need IDs against. Only applies when id_required is true.

id_length

Type: integer (default: 5)

Length of auto-generated IDs when not explicitly provided.

id_from_title

Type: boolean (default: false)

When true, auto-generated IDs will be based on the need title instead of being random.

title_optional

Type: boolean (default: false)

When true, allows needs to be created without a title.

title_from_content

Type: boolean (default: false)

When true, auto-generates titles from the first line of need content when no title is provided.

default_layout

Type: string (default: "")

The card design every need with no :layout: of its own is drawn with.

The value is any design this project knows — see the list under [needs.fields.layout] for what that means, and Card designs with [needs.card_layouts] for what each design shows. A :layout: written on the need itself wins over this, and so does a value a [needs.fields.layout] default or predicates row puts on the need: this key only catches needs that end up with no value at all. An empty string is the same as unset, which is the same as clean — the design that draws ubCode’s ordinary card. A value naming no known design is reported and treated as unset.

[needs]
default_layout = "complete"

Moving the default changes what writing nothing means, so :layout: clean stops being a no-op and becomes a deliberate override: under the example above, a need with no :layout: draws complete and a need with :layout: clean draws clean.

This is the ubCode spelling of Sphinx-Needs’ needs_default_layout, and it is the same key in the same table: needs_from_toml maps [needs] default_layout onto needs_default_layout, so a ubproject.toml shared between sphinx-build and ubc build html now means one thing to both. Two behaviours differ deliberately — an empty string is unset here and a build error there, and an unknown name is reported here rather than aborting the build; see Differences from a Sphinx build.

views_max_items

Type: integer (default: 100)

The default :max_items: cap for the view directives (needlist, needtable, needflow and needsequence).

A view that sets no :max_items: of its own renders at most this many items and appends a notice saying how many of how many are shown; a view that sets one uses its own value instead. Set 0 for no cap — from either source. The cap applies after the filter and the sort, in both the editor’s rendered preview and ubc build html. A build also emits a build.need_view_truncated warning for each truncated view, so a capped view is visible in the build log without opening the built page.

part_prefix

Type: string (default: "→ " — a right arrow and a non-breaking space)

The text put in front of a need part’s ID and content in a needtable that sets :show_parts:.

It marks a part’s row as belonging to the need above it. The default’s space is non-breaking on purpose, so the marker cannot wrap away from what it labels. Set "" for no prefix at all. It applies in the editor’s rendered preview and in ubc build html alike, and nothing but a :show_parts: table reads it.

This is the ubCode spelling of Sphinx-Needs’ needs_part_prefix, with the same default.

report_template

Type: string (default: "")

A Jinja template file that every .. needreport:: in the project renders from, instead of ubCode’s built-in report. A relative path is taken from the directory holding ubproject.toml; an absolute one is used as written. Unset — or set to "" — renders the built-in report.

A directive’s own :template: option wins over this key, and resolves differently: relative to the document that writes it, with a leading / meaning relative to the project source root — which it may not leave. A :template: that resolves outside the source root is refused (needs.report_template_outside_project) and the built-in report renders instead; this key is the supported way to use a template that lives outside it, because you write it in the project’s own configuration file rather than in a document. (With no ubproject.toml at all there is no configured source root, and the boundary is the host document’s own directory — the diagnostic says which of the two it used.)

[needs]
report_template = "templates/report.need"

The template must produce reStructuredText, and is given the same five names a Sphinx-Needs build supplies:

Name

Value

types

one entry per configured need type, in declaration order, with the keys directive, title, prefix, color and style

links

one entry per configured link type, with the keys option, incoming, outgoing, copy and allow_dead_links

options

the project’s extra field names

usage

needs_amount and needs_types (a count per configured type)

report_directive

the string dropdown

A section the directive did not ask for arrives as the empty value rather than as a missing name, so a template can always reference all five and guard with {% if types|length != 0 %}.

{% if types|length != 0 %}
.. dropdown:: Need Types
{% for type in types %}
   * {{ type.title }} (``{{ type.directive }}``)
{%- endfor %}
{% endif %}

Requirements: :need_count:`type == "req"`.

Two practical notes:

  • Give the file an extension your project does not build.need or .txt, not .rst or .md — or it is also indexed as a document of its own. (The Sphinx-Needs documentation gives the same advice.)

  • Python method calls work in every ubCode Jinja surface, this one included: link.get('copy', None), d.items() and s.upper() all resolve, so a template written for a Sphinx-Needs build runs unchanged.

If the template cannot be used, the directive renders the built-in report and says why — needs.report_template_not_found, needs.report_template_outside_project or needs.report_template_failed.

flow_direction

Type: string (default: "down")

The default layout direction for needflow diagrams — where the graph grows.

One of down, up, right or left, or the equivalent engine tokens TB/TD, BT, LR and RL. A needflow that sets its own :direction: uses that instead. An empty string is the same as unset.

flow_show_links

Type: string or boolean (default: "none")

The default edge labelling for needflow diagrams.

outgoing and incoming label each edge with the link type’s corresponding display name, type labels it with the link type’s own name, and none leaves edges unlabelled. true means outgoing and false means none, so a value carried over from Sphinx-Needs’ needs_flow_show_links means the same thing here. A needflow that sets its own :show_link_names: uses that instead, including :show_link_names: none to leave one diagram unlabelled in a project that labels edges by default. An empty string is the same as unset.

flow_show_legend

Type: string (default: "")

Which named legend a needflow gets when its :show_legend: names none.

The value is a key of flow_legends below. This selects which legend a diagram gets, never whether it gets one: only a :show_legend: on the directive itself draws a legend, so setting this cannot add a legend to a diagram that did not ask for one. A needflow whose :show_legend: names a key uses that key instead. An empty string is the same as unset, which leaves the choice to the diagram engine — for the Mermaid flowcharts ubCode draws, a table beside the diagram listing the drawn need types. A value naming no configured legend is reported and treated as unset.

flow_legends

Type: table of tables (default: none)

The project’s named legend configurations, each selected by name from a needflow’s :show_legend: or by flow_show_legend above.

[needs.flow_legends.wide]
parts = ["types", "links"]
placement = "external"
parts

The sections the legend lists, as a list, in the order they are drawn: types (each drawn need type with its colour) and links (each drawn link type with its display name). Unset is the same as ["types"].

Order is significant. parts = ["links", "types"] draws the link table first. That is why this key is a list and not a single name: a name per combination could not express order without inventing one per permutation.

parts = ["types"]            # just the need types
parts = ["types", "links"]   # both tables, types first
parts = ["links", "types"]   # both tables, links first

A list is the only accepted spelling. A bare string is reported, with the list form to write instead.

placement

Where the legend is drawn: internal (inside the diagram itself) or external (a table beside it). Unset takes the diagram engine’s default placement, which for the Mermaid flowcharts ubCode draws is external.

internal is a preference, not a guarantee. An engine that cannot draw a legend inside the diagram draws the external one instead, with the same rows. Mermaid is such an engine — its flowchart syntax has no legend construct — so ubCode always draws the external table and says nothing about it. The key is still worth setting for a project whose ubproject.toml is shared with a Sphinx-Needs build, where internal is honoured and is that engine’s default.

Every mistake in a legend costs only what it names, and the legend still renders: an unknown section is reported and skipped, a repeated one is reported and kept once, and an empty or non-list parts is reported and takes the default. A legend name that could never be selected — one that is empty or has surrounding whitespace — is reported and the entry is ignored.

diagram_template

Type: string (default: "")

The per-node Jinja template that a .. needuml:: or .. needarch:: body’s flow() renders each need with. Unset — or set to "" — uses ubCode’s built-in template, which is byte-identical to the Sphinx-Needs default.

[needs]
diagram_template = """{{ need.type_name }} {{ need.id }}"""

The template produces PlantUML, is given the need as need, and sees every name [needs.render_context] adds.

This is the ubCode spelling of Sphinx-Needs’ needs_diagram_template, with the same default. ubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, so a template that emits a PlantUML construct Mermaid has no equivalent for is reported (needs.uml_unconvertible) rather than drawn.

[needs.render_context]

Type: table of string → value (default: empty)

Extra names every template ubCode executes can read — a needuml or needarch body, diagram_template, and the needreport report_template.

[needs.render_context]
product = "ubCode"
release = "2026.1"

Names here do not reach two template surfaces, deliberately: [needs.string_links] templates and needs_role_need_template. Both see their pattern’s named capture groups and nothing else, which is also what keeps a project-wide name from silently shadowing a capture group called value.

This is the ubCode spelling of Sphinx-Needs’ needs_render_context.

[needs.flow_configs]

Type: table of stringstring (default: empty)

Named blocks of PlantUML configuration that a .. needuml:: or .. needarch:: pulls in with its :config: option. Each value is a block of PlantUML lines; the option names one or more blocks, comma-separated, and their lines are prepended to the produced diagram.

[needs.flow_configs]
monochrome = """
skinparam monochrome true
skinparam shadowing false"""
.. needuml::
  :config: monochrome

  {{ flow('R_LOGIN') }}

A :config: naming a block this table does not define is reported (needs.uml_config_unknown) and contributes nothing; a Sphinx build is silent about it, which leaves a diagram that renders perfectly and merely looks wrong.

Because ubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, a block’s lines reach the diagram but only the ones Mermaid has an equivalent for change what is drawn — skinparam in particular is dropped. Adding :debug: to the directive shows the produced PlantUML with the block’s lines in place, which is the way to check that a name resolved.

This is the ubCode spelling of Sphinx-Needs’ needs_flow_configs, with one scope difference: there the key is also read by needflow, and here it is not, because ubCode’s flow engine is not PlantUML.

Need types

The types array defines the available need directives in your project:

[[needs.types]]
directive = "requirement"  # The RST directive name
title = "Requirement"     # Human-readable title
prefix = "REQ_"          # Prefix for auto-generated IDs
color = "#BFD8D2"        # Color for diagrams and the need-card accent
style = "node"           # Style for diagrams

Each type supports:

  • directive (required): The reStructuredText directive name

  • title (required): Human-readable title for the need type

  • prefix (required): Prefix used for auto-generated need IDs

  • color (optional, default: "#000000"): Color used in need diagrams and as the accent on this type’s need cards, where it outlines the type badge and paints the card’s leading edge. An explicit *_border style frames all four edges and takes that one; on cards that show a type badge, the badge outline still carries the type colour (the focus layout family shows no badge, and a hidden card’s badge outline is zero-width). The card accent applies only to a colour you have chosen — see When a type colour accents a card below. Cards use only #RGB, #RRGGBB and #RRGGBBAA values; any other value is ignored for the card. Diagrams are unaffected and always use the color.

  • style (optional, default: "node"): Style used in need diagrams

When a type colour accents a card

A type’s color accents its need cards only where you have chosen it. Two cases are deliberately not treated as a choice:

  • a type declared without a color — the #000000 default means no accent rather than black. So do #000, #000000FF and any fully transparent value (an 00 alpha, e.g. #00000000), since an invisible accent is not one. A partly transparent colour such as #00000080 is a choice, and paints. For a near-black accent write #010101.

  • a built-in type still carrying its shipped colour. Every project gets the four built-ins whether or not it asks, and their colours were picked to fill diagram nodes, so accenting them would colour every card of every project:

    Directive

    Shipped color

    Accents a card?

    req

    #BFD8D2

    no — this exact pair is the shipped default

    spec

    #FEDCD2

    no

    impl

    #DF744A

    no

    test

    #DCB239

    no

    The rule matches the pair, so these colours accent normally on any other directive, and any other colour accents normally on these four.

Note

Migrating from sphinx-needs? Those four values are sphinx-needs’ own defaults, so a [[needs.types]] block copied across lands on them exactly — and your cards will show no accent. If a colour of yours is being ignored, change it by one digit (#BFD8D2#BFD8D3) and it will paint. Spelling does not help: the comparison ignores case and a fully opaque alpha, so #bfd8d2 and #BFD8D2FF on req are the same pair.

Fields

Every need carries fields — named pieces of metadata such as status, tags, or any custom attribute you define. Some fields are built in (id, title, status, tags); you can add your own through the configuration.

ubCode supports two formats: the dict-based [needs.fields] format (aligned with sphinx-needs ≥ 7) and the list-based extra_options format (for sphinx-needs < 7).

Note

Three names cannot be shadowed by a custom field: id and type are refused outright, and content — the need’s own body — is reported as config.field_shadowed_by_identity. A content field is still created and can still be set on a directive, but every filter, table, variant condition, report template and needs.json key answers the body for that name, so the declared value is invisible to every one of them. See Variants for the shadowing rule in full.

Dict-based format (recommended)

Note

The dict-based [needs.fields] format requires sphinx-needs ≥ 7.0. It replaces the older list-based needs_extra_options and also allows specializing core fields (e.g. status, tags) by narrowing their type constraints.

Use [needs.fields.<name>] to define each custom field:

[needs.fields.priority]
description = "Priority level"
default = "medium"

[needs.fields.assignee]
description = "Assigned team member"

[needs.fields.effort]
description = "Story points"
schema = {type = "integer", minimum = 1, maximum = 13}

Each field supports:

  • description (optional): Human-readable description

  • default (optional): Default value if not specified on a need

  • schema (optional): JSON Schema to validate values (see Field schemas below). Schema support for fields was introduced in sphinx-needs 6.0 (via ``needs_extra_options``) and carried forward to ``needs_fields`` in sphinx-needs 7.0.

  • nullable (optional): Whether the field accepts null values. sphinx-needs ≥ 7.0.

  • predicates (optional): Conditional defaults (see Global options). sphinx-needs ≥ 7.0.

  • parse_variants (optional): Whether to parse variant functions (<<...>>) and variant data references (<{ ... }>, see Variant data) in this field’s values. sphinx-needs ≥ 7.0.

  • parse_dynamic_functions (optional): Whether to parse dynamic functions in this field’s values. sphinx-needs ≥ 6.3.

Once declared, these fields can be used on any need:

.. req:: Encrypt data at rest
   :id: REQ_002
   :status: open
   :priority: high
   :assignee: alice
   :effort: 5

Styling cards with [needs.fields.style]

style is an ordinary core field, so the same default and predicates keys give a project-wide card style and per-condition ones — this is ubCode’s replacement for sphinx-needs’ needs_default_style, and no separate configuration key is needed:

[needs.fields.style]
# Every card starts discreet …
default = "discreet"
# … except that closed needs go green and blocked ones get a red frame.
predicates = [
    ['status == "closed"', 'green'],
    ['status == "blocked"', 'red_border'],
]

A :style: written on the need itself wins over both. The value is a comma-separated list of tokens; each becomes a ub-need-style-<token> class on the card, in the order written and with duplicates collapsed. ubCode ships styling for the twenty sphinx-needs palette names — the green / red / yellow / blue / gray / discreet fills (with implemented / open / in_progress as aliases), the green / red / yellow / blue / discreet _border frames, the green / red / orange / yellow / blue _bar stripes, and clean. A token that is not a valid CSS identifier is ignored and reported as build.need_style_invalid; see Differences from a Sphinx build for how this differs from a Sphinx-Needs build.

Choosing a card design with [needs.fields.layout]

layout is an ordinary core field too, so the same two keys give a project-wide card design and per-condition ones — which is more than Sphinx-Needs can express, where the card design is one global value:

[needs.fields.layout]
# Every card is the default design …
default = "clean"
# … except that specifications show everything, and needs marked
# `:hide_meta:` show only their content.
predicates = [
    ['type == "spec"', 'complete'],
    ['hide_meta == True', 'focus'],
]

Note

Earlier versions of this page said that this made a separate configuration key unnecessary. That was wrong in the one case that matters. A ubproject.toml is read by both engines, and Sphinx-Needs’ needs_from_toml already maps [needs] default_layout onto needs_default_layout while silently skipping [needs.fields] entirely — so a project that wrote its card design the field way got it from ubc build html and not from sphinx-build, with no warning from either. ubCode therefore has [needs] default_layout as well, and it is the key to reach for when one file feeds both engines.

The two mechanisms are not competing tiers of one chain, and the precedence follows from what each one does:

  1. a :layout: written on the need itself wins over everything;

  2. then a [needs.fields.layout] predicates row, then its default — both write a value onto the need, which is why they beat the key below;

  3. then [needs] default_layout, which catches only the needs that end up with no value at all;

  4. then clean.

Use default_layout for the project-wide design — it is the spelling both engines read — and [needs.fields.layout] predicates for the per-condition exceptions, which have no Sphinx-Needs equivalent. The value is any design this project knows: one of the ten built-in ones — clean (the default), complete, focus, debug, and the six side-column names clean_l, clean_lp, clean_r, clean_rp, focus_l and focus_r — one of the two sphinx-needs identities (test, focus_f), or a design the project defines for itself with [needs.card_layouts]. An unknown name is reported as build.need_layout_unknown and the card renders with the default design; see Card designs with [needs.card_layouts] for what each design shows and how to define your own, and Differences from a Sphinx build for how this differs from a Sphinx-Needs build.

Hiding cards conditionally with [needs.fields.hide]

hide is an ordinary core field as well, and a boolean rather than a string — so default and predicates take true / false, and give you conditional hiding: hide every draft need, or every need of a type, without writing :hide: on any of them. This is parity rather than divergence — a Sphinx-Needs build reaches the same result through needs_fieldsdefault / predicates on the hide core field, which upstream flags allow_default for exactly this purpose:

[needs.fields.hide]
# Nothing is hidden by default …
default = false
# … except needs still being drafted, and the ones an internal
# process owns rather than the document.
predicates = [
    ['status == "draft"', true],
    ['type == "internal"', true],
]

A :hide: written on the need itself wins over both. A hidden need renders its whole card inside a closed disclosure — one compact summary line the reader can expand in place — so nothing is withheld, and references, link rows, needlist entries and the built site’s search index all still reach it; see Differences from a Sphinx build for the full behaviour and for how this differs from a Sphinx-Needs build.

Hiding a need does not remove it from the project, nor its content from the page: it stays in the index, in needs.json, in every filter result and in the published HTML, merely collapsed. :hide: is not a confidentiality boundary; use :delete: for a need that should produce no output at all.

List-based format (sphinx-needs < 7)

Deprecated since version 0.25.0: The list-based extra_options format (needs_extra_options in sphinx-needs) was deprecated in sphinx-needs 7.0 in favour of the dict-based format. It remains supported for backward compatibility. For new projects, prefer the dict-based [needs.fields] format.

Simple format (string list):

extra_options = ["priority", "effort", "assignee", "component"]

Detailed format (with descriptions and schemas):

extra_options = [
    {name = "priority", description = "Priority level"},
    {name = "effort", description = "Story points", schema = {type = "integer", minimum = 1, maximum = 13}},
    {name = "assignee", description = "Assigned team member"},
]

Field schemas

The schema key on a field applies JSON Schema validation to every value entered for that field. Invalid data is caught at index time, before the docs are built.

String schemas:

[needs.fields.contact]
schema = {type = "string", format = "email"}

[needs.fields.id_pattern]
schema = {type = "string", pattern = "^[A-Z]{3}-[0-9]{4}$"}

[needs.fields.severity]
schema = {type = "string", enum = ["low", "medium", "high", "critical"]}

String format options:

The format property supports various predefined formats:

  • "date": RFC 3339 date format (e.g., 2023-10-22)

  • "date-time": RFC 3339 date-time format (e.g., 2023-10-22T14:30:00Z)

  • "time": RFC 3339 time format (e.g., 14:30:00Z)

  • "email": RFC 5322 email address (e.g., user@example.com)

  • "hostname": RFC 1123 hostname (e.g., example.com)

  • "uri": RFC 3986 URI (e.g., https://example.com/path)

  • "uuid": RFC 4122 UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)

  • "regex": ECMAScript regular expression

Integer and number schemas:

[needs.fields.priority]
schema = {type = "integer", minimum = 1, maximum = 10}

[needs.fields.rating]
schema = {type = "number", minimum = 0.0, maximum = 5.0}

[needs.fields.fibonacci]
schema = {type = "integer", enum = [1, 2, 3, 5, 8, 13, 21]}

Boolean schemas:

[needs.fields.approved]
schema = {type = "boolean"}

Array schemas:

[needs.fields.reviewers]
schema = {type = "array", items = {type = "string", format = "email"}, minItems = 1, maxItems = 3}

[needs.fields.categories]
schema = {type = "array", items = {type = "string"}, uniqueItems = true}

Usage in need directives:

Once defined, these fields can be used in your need directives:

.. requirement:: User Authentication
    :priority: 5
    :effort: 8
    :assignee: developer@company.com
    :due_date: 2024-12-31
    :tags: security, authentication
    :component: auth, backend
    :active: true

    Users must be able to authenticate using their corporate credentials.

Statuses and tags

You can constrain the allowed values for a need’s status and tags fields. The recommended approach depends on your sphinx-needs version.

Legacy list-based constraints (sphinx-needs < 7)

Deprecated since version 0.25.0: The [[needs.statuses]] and [[needs.tags]] list formats (needs_statuses / needs_tags in sphinx-needs) were deprecated in sphinx-needs 7.0 in favour of schema enum constraints on the status and tags fields (see above). They remain supported for backward compatibility.

[[needs.statuses]]
name = "draft"
description = "Work in progress"

[[needs.statuses]]
name = "approved"
description = "Reviewed and approved"

[[needs.tags]]
name = "security"
description = "Security-related needs"

External needs

Load needs from external sources (JSON files or URLs):

# From local file
[[needs.external_needs]]
json_path = "external/project_needs.json"
id_prefix = "EXT"
base_url = "https://external-project.com"
target_url = "docs/{{ id }}.html"

# From remote URL
[[needs.external_needs]]
json_url = "https://api.example.com/needs.json"
id_prefix = "REMOTE"

Each external needs source supports:

  • json_path: Path to a local JSON file

  • json_url: URL to a remote JSON file

  • id_prefix: Prefix prepended to external need IDs

  • base_url: Base URL for linking back to the source project

  • target_url: URL template for individual need links (supports {{ id }} placeholder)

  • css_class: CSS class applied to external needs

  • version: Version string for the external needs source

  • skip_in_ubcode (default: false): When true, skip loading these external needs in ubCode analysis

Remote sources

The [needs.remote] section controls how ubCode fetches needs.json over HTTP(S). It applies to every remote source: [[needs.external_needs]] entries with a json_url, and needimport directives whose argument is a URL. Local sources — a json_path, or a file:// URL, which is read straight from disk — are unaffected by every key here.

[needs.remote]
cache_days = 1
timeout = 10.0
offline = false
  • cache_days (default: 1): How long, in days, an already-loaded remote source’s needs are reused before ubCode re-checks the source. 0 re-checks on every pass, and a negative value never re-checks. A re-check sends the ETag / Last-Modified of the last response, so an unchanged source costs one small request and its needs are kept as they are.

  • timeout (default: 10.0): Timeout, in seconds, for each request.

  • offline (default: false): When true, never fetch remote sources. Each one keeps the needs it was last loaded with and is reported as skipped — useful in network-restricted CI, or for working offline against an index built while online.

A remote source whose fetch fails keeps the needs it loaded last time, rather than losing them until the next success, and the failure is reported. Within one long-running process — the language server, or a watch loop — an unreachable host is not re-attempted for five minutes, so an editing session against a source that is down does not pay a connection timeout on every pass. That backoff is per-process and is not persisted, so a one-shot ubc build or ubc check against an unreachable host waits for timeout on every run; lower timeout, or set offline = true, if that is the situation you are in.

Variants

The [needs.variants] section defines named filter expressions that can be referenced from need fields using the <<name: value, fallback>> syntax. This implements the Sphinx-Needs variant functions feature.

[needs.variants]
is_open = "status == 'open'"
is_html = "'html' in build_tags"
is_production = "environment == 'production'"

With these variants defined, need fields can use them:

.. req:: My requirement
   :id: REQ_001
   :status: <<is_open: active, inactive>>
   :extra: <<is_html: web_only, general>>

You can also use inline bracketed expressions without pre-defining a variant:

.. req:: Another requirement
   :id: REQ_002
   :status: <<[status == 'open']: active, inactive>>

The last comma-separated value is always the fallback when no expression matches.

A variant expression resolves the need’s own fields and links, the project’s filter_data keys, the build_tags of the current build, and the context fieldscontent, docname, lineno, sections, section_name, is_external, is_directive, is_import, is_src_trace, is_modified, modifications, is_need, is_part — the same names a :filter: expression resolves:

.. req:: Section-aware requirement
   :id: REQ_003
   :status: <<[section_name == 'Interfaces']: interface, internal>>

A project-declared field or link of the same name wins over a context field, except for id, type and content, which cannot be shadowed. Inside a variant expression is_modified and modifications count needextend matches only — a need’s own variant resolution cannot be an input to itself.

A name may also carry the n. prefix (<<[n.status == 'open']: a, b>>), matching what a :filter: expression accepts; any other namespace is reported as an unknown name.

Note

Variant syntax is only parsed for fields that opt in. In sphinx-needs ≥ 7.0, set parse_variants = true on individual fields in [needs.fields]. For older versions, use the deprecated variant_options list (see Deprecated needs options).

Parse and evaluation errors are surfaced as needs.variant diagnostics, including a name the expression cannot resolve — which is reported with a “did you mean” suggestion, while the expression still resolves to its fallback value.

See also

Variant data for the related var.* namespace and the <{ ... }> reference syntax, which inject build-wide data rather than selecting between values conditionally.

Variant data

sphinx-needs ≥ 8.2.

Variant data is a nested, read-only key-value store that ubCode exposes under the var.* namespace inside filter expressions. It lets a single “150%” model — one set of needs that describes every product variant — be filtered, extended, and rendered differently for each build, without duplicating content.

See also

Variant builds walks through the end-to-end workflow — including building and diffing each variant from the command line. This section is the configuration reference for the options it uses.

For example, a project that targets several CPU architectures and build configurations can keep one copy of its requirements and select the data relevant to the current build:

[needs.variant_data]
platform = "arm"
debug = false
opt_level = 2
archs = ["arm", "x86"]

[needs.variant_data.build]
compiler = "clang"
features = ["networking", "logging"]

With this configuration the following values are available in any filter expression:

var.platform == "arm"                 # scalar string
var.opt_level == 2                    # scalar integer
var.debug == False                    # scalar boolean
"x86" in var.archs                    # membership in an array
var.build.compiler == "clang"         # nested scalar
"networking" in var.build.features    # membership in a nested array

Note

var.* resolves only against the configured variant data. Unlike need fields or filter_data keys, it does not depend on the need currently being evaluated, which makes it suitable for global, build-wide switches. Looking up an unknown key (e.g. var.missing) is a needs.variant diagnostic, not a silent false.

Defining variant data

Variant data can be provided inline, from a JSON file, or both.

variant_data

Type: object (default: {})

An inline, nested table of variant data, declared directly in ubproject.toml under [needs.variant_data] (as shown above).

variant_data_file

Type: string (default: "")

Path to a JSON file containing variant data, resolved relative to the ubproject.toml that declares it. This is convenient when the values are generated by a build system or shared across several projects.

[needs]
variant_data_file = "variant_data.json"
{
  "platform": "linux",
  "build": {"compiler": "gcc", "features": ["networking"]}
}

When both are set, the file is loaded first as the base, and the inline [needs.variant_data] values are deep-merged on top, with the inline values winning:

  • At nested maps, keys are merged recursively.

  • At scalars and arrays, the inline value replaces the file value entirely.

[needs]
# file sets platform = "linux", build.compiler = "gcc", build.features = [...]
variant_data_file = "variant_data.json"

[needs.variant_data]
platform = "windows"   # overrides the file's scalar value

[needs.variant_data.build]
compiler = "clang"     # overrides build.compiler;
                       # build.features is preserved from the file

Because inline values take precedence, you can keep shared defaults in the file under version control and override individual switches per build.

Value types

Variant data values may be:

  • Scalars — string, integer, number (float), or boolean.

  • Uniform arrays of scalars — every element must share the same type (mixed-type arrays are reported as a needs.variant diagnostic).

  • Nested maps — arbitrarily deep tables, accessed with further dots (var.build.sanitizers.asan).

Using var.* in filters

The var.* namespace is available anywhere ubCode evaluates a filter expression against your needs configuration.

Variant functions — select a field value per build (see Variants):

.. req:: Bootloader
   :id: REQ_001
   :status: <<[var.platform == "windows"]: active, inactive>>

Conditional defaults — vary a field’s default via predicates:

[needs.fields.compiler_note]
default = "unknown-compiler"
predicates = [
    ['var.build.compiler == "clang"', "using-clang"],
    ['var.build.compiler == "gcc"', "using-gcc"],
]

needextend — extend only the needs relevant to the current build:

.. needextend:: var.platform == "windows"
   :status: supported

needimport — import only the needs that match:

.. needimport:: external.json
   :filter: var.platform == "windows"

External needs — filter needs loaded from another project:

[[needs.external_needs]]
json_path = "imported.json"
filter = 'var.platform == "windows"'

Referencing variant data in field values (<{ ... }>)

In addition to filtering, variant data can be injected directly into a field or link value using the <{ ... }> syntax. The text between the delimiters is a var.* path, and ubCode substitutes the stored value at index time.

Unlike variant functions (<<...>>), which choose between candidate values using conditional logic, a <{ ... }> reference is a direct lookup — there is no condition, just a substitution.

This syntax is only parsed for fields and links that opt in with parse_variants = true (the same flag that enables <<...>>):

[needs.variant_data]
platform = "arm"
links = ["REQ_001", "REQ_002"]

[needs.variant_data.build]
opt_level = 2

[needs.fields.arch]
schema = {type = "string"}
parse_variants = true

[needs.fields.opt]
schema = {type = "integer"}
parse_variants = true

[needs.links.derived_from]
parse_variants = true

The references can then be used on any need:

.. req:: Scalar reference
   :id: REQ_001
   :arch: <{ var.platform }>
   :opt: <{ var.build.opt_level }>

.. req:: Embedded in a string
   :id: REQ_002
   :arch: built for <{ var.platform }>

.. req:: Within an array and a link field
   :id: REQ_003
   :tags: a, <{ var.platform }>, c
   :derived_from: <{ var.links }>

A few rules apply:

  • For string fields the reference may be embedded inside surrounding text; for non-string scalar fields (integer, number, boolean) the reference must be the entire value.

  • The resolved value is type-checked against the field’s schema.

  • If the field does not set parse_variants = true, the <{ ... }> text is kept literally.

The following situations are reported as needs.variant diagnostics (and leave the value unset):

  • Invalid expression — the content is not a dotted var.* path, e.g. <{ platform }> (missing the var. root).

  • Unknown key — the path does not exist in the variant data, e.g. <{ var.nonexistent }> or <{ var.build.missing }>.

  • Non-leaf value — the path resolves to a nested map rather than a scalar or array, e.g. <{ var.build }>.

  • Type mismatch — the resolved value’s type does not match the field, e.g. injecting a string into an integer field.

See also

The corresponding sphinx-needs feature is documented under variant data.

Dynamic functions

ubCode resolves the built-in dynamic functions using the [[...]] syntax from sphinx-needs.

All six built-in functions are supported:

  • echo("text") — returns the given string unchanged

  • test(*args, **kwargs) — returns a debug string with the need ID and arguments

  • copy("field", need_id=None) — copies a field value (also supports lower and upper kwargs). field may be any of the need’s own fields or links, or a context fieldid, type, content, docname, lineno, sections, section_name, parent_need, is_external, is_directive, is_import, is_src_trace, is_modified, modifications, is_need, is_part. A project-declared field of the same name wins, except for id, type and content. need_id — as a keyword or as the second positional argument — copies from that need instead of the current one

  • links_from_content(need_id=None) — returns the needs referenced via :need: roles in a need’s own content, the current need’s unless need_id names another. need_id may be given as a keyword or as the single positional argument

  • check_linked_values(result, search_option, search_value, one_hit=False) — compares the search_option field of every need this one :links: to against search_value. Without one_hit, result is returned only when they all match — and also when the need links to nothing at all. With one_hit, result is returned whatever the values are: that is a quirk of Sphinx-Needs itself, mirrored here deliberately so the two tools agree. Otherwise the call yields nothing. It fails only on a broken link, or on a search_option that is not a field, link or context field of the linked need — a failed call renders ?? in a role or :style_row:, and is skipped silently in a field or link value

  • calc_sum("option", links_only=False) — sums a numeric field across the project’s needs, or across only the needs this one links to when links_only is set

The filter argument (filter_string on check_linked_values) is not yet supported. A call that passes one is reported where it is written, and what you see in its place depends on where you wrote it: a role keeps rendering as the role chip — its own text, unevaluated; a [[...]] written in a needtable’s :style_row: renders ??; and a [[...]] written in a need’s field or link value is skipped silently, leaving the field at its other values. (A bare [[...]] in a need’s body content is not evaluated at all — use the ndf role there.) Functions a project registers through needs_functions are not resolved.

For example:

.. req:: My requirement
   :id: REQ_001
   :status: [[copy("priority")]]

.. spec:: My specification
   :id: SPEC_001
   :links: [[links_from_content()]]

   This specification realises :need:`REQ_001`.

Note

In a need’s own field or link value, copy('is_modified') and copy('modifications') count needextend matches only — the same caveat the Variants section carries, and for the same reason: a need’s own resolution cannot be an input to itself. Every other context field resolves as a :filter: resolves it.

In prose: the ndf and need_func roles

The same functions can be called from inside prose, and the result is rendered in place as plain text:

A dynamic function :ndf:`echo("in a paragraph")` reads inline.

.. req:: My requirement
   :id: REQ_002

   This need's own id is :ndf:`copy("id")`.

ndf takes the bare call. need_func takes text containing a [[...]] call and substitutes the result into that text, keeping what surrounds it:

:need_func:`before [[echo("x")]] after`

renders before x after. Upstream deprecated need_func in favour of ndf in sphinx-needs 3.1.0; ubCode resolves both.

Which need a call reads depends on where it is written. Inside a need’s body, the call reads that need, so copy("id") above renders REQ_002. Outside any need there is no current need, so a function that needs one — copy and links_from_content — renders ??. A Sphinx build renders ?? there too, but it also logs a warning saying which function failed and why; ubCode renders the ?? silently. That difference is deliberate and is recorded under When ubCode cannot resolve a call. echo and test need no need and resolve anywhere.

MyST works the same way, with the role spelled {ndf} / {need_func}.

Note

A need_func whose text contains no [[...]] at all renders that text literally and reports nothing — which is what a Sphinx build does with it too.

When ubCode cannot resolve a call

A call that cannot resolve for any need is reported once, at the place it is written, under the needs.dynamic_functions code. Three things trigger it: the text does not parse as a function call, it names a function ubCode does not implement, or it names one that cannot be called that way. The report covers need field and link values, a needtable :style_row:, and the two roles above.

These findings are informational, not warnings, and that is deliberate: ubCode implements every built-in, but a Sphinx project also has everything needs_functions registers and ubCode cannot run project Python, so an unresolvable call is often a statement about ubCode rather than about the document. They are reported, they do not fail ubc check, and they can be silenced like any other code:

[lint]
ignore = ["needs.dynamic_functions"]

A role whose call is reported this way keeps rendering as the role chip — its own text, unevaluated — rather than as ??. ?? would claim the call ran and failed, which is not true of a function a Sphinx build resolves and ubCode does not. A call that ubCode does implement but that fails when it runs — copy("status") on a need that has no status, or any call needing a need when there is none — renders ?? and is not reported.

A Sphinx build reports these: it renders the same ?? and logs, for example, Error while executing function 'copy': Option status not found in need REQ_002. ubCode is silent, and that is a known gap rather than a considered quietness: the value is computed while the page is being rendered, which is after the point where ubCode attaches findings to a source location. Reporting it is planned. Everything ubCode can decide from the text you wrote — an unparseable call, an unknown function name, a bad argument list — is reported, once, at the place you wrote it.

Dynamic function parsing is enabled by default and can be controlled globally or per-field/per-link:

[needs]
parse_dynamic_functions = false  # disable globally (sphinx-needs ≥ 7.0)

[needs.fields.my_field]
parse_dynamic_functions = true   # re-enable for this field (sphinx-needs ≥ 6.3)

[needs.links.implements]
parse_dynamic_functions = true   # re-enable for this link (sphinx-needs ≥ 6.3)
parse_dynamic_functions

Type: boolean (default: true)

Whether dynamic functions are parsed by default. Applies to extra fields and links unless overridden per-field/per-link. Core fields are not affected by this global setting.

The global parse_dynamic_functions option was added in sphinx-needs 7.0 (needs_parse_dynamic_functions). Per-field and per-link parse_dynamic_functions was added in sphinx-needs 6.3.

filter_data

Type: object (default: {})

Additional context variables that can be referenced in filter expressions throughout your needs configuration. This allows you to define custom variables for conditional logic in multiple contexts.

Where filter_data can be used:

  1. Predicate default filters — in global_options predicates (or predicates on individual fields/links in sphinx-needs ≥ 7.0) to conditionally set default values

  2. needimport item filters — to filter which needs are imported from external sources

  3. needextend filters — to conditionally extend or modify needs based on context

[needs]
filter_data = { environment = "production", team = "backend", version = "2.0" }

# Using predicates on a field directly (sphinx-needs ≥ 7.0, recommended)
[needs.fields.priority]
default = "medium"
predicates = [
  ["environment == 'production'", "high"],
  ["team == 'backend'", "high"]
]

# Or via global_options (sphinx-needs < 7, deprecated)
# [needs.global_options.priority]
# default = "medium"
# predicates = [
#   ["environment == 'production'", "high"],
#   ["team == 'backend'", "high"]
# ]

Example with needimport:

.. needimport:: external_needs.json
   :filter: environment == 'production' and version == '2.0'

Example with needextend:

.. needextend:: team == 'backend'
   :status: critical

Common use cases:

  • Setting environment-specific defaults (development, staging, production)

  • Filtering imported needs based on release version or team ownership

  • Applying team or project-specific configurations

  • Conditional formatting or validation based on build context

  • Dynamic behaviour in CI/CD pipelines

import_keys

Type: object (default: {})

Mapping of keys for needimport directives, allowing short keys to reference longer file paths. Available since sphinx-needs 4.2.

[needs.import_keys]
backend = "/path/to/backend_needs.json"
frontend = "/path/to/frontend_needs.json"

JSON output

These keys shape the needs.json export. They apply to whatever writes that file — ubc build needs (the default --format json), and ubc build html when build_json is on — so the shaping below is identical whichever command wrote the file.

The two commands still differ in one respect these keys do not govern: ubc build needs writes source maps by default (an __source__ entry per need, recording where each need was written), while the build_json sidecar omits them, which is closer to what Sphinx-Needs writes.

Note

The keys do not shape ubc diff. That command projects the index through the same writer with no shaping applied, so an excluded field is still compared. Note that its link comparison is currently condition-blind: it reads link values without their [condition] suffixes, so editing only a link’s condition does not register as a change. Splitting the two uses apart — conditions for the field-by-field comparison, bare IDs for the impact graph, which keys nodes on them — is a separate piece of work.

build_json

Type: boolean (default: false)

When true, ubc build html also writes needs.json into the root of the output directory, beside the pages. Clearing the key removes the copy an earlier build wrote, provided the output directory is one ubCode owns (one it created itself, or an empty one) — in a directory it does not own it never deletes anything, so a stale export would have to be removed by hand.

Warning

In an output directory ubCode owns, needs.json at the root is a builder-managed name, like the generated pages and the _static/ and _images/ trees. A file you place there yourself under that name is removed by the next build, whether or not build_json is set. Keep a hand-maintained needs.json outside the output directory.

[needs]
build_json = true

json_exclude_fields

Type: array of string

Default:

json_exclude_fields = [
  "lineno_content", "collapse", "hide",
  "type_prefix", "type_color", "type_style",
  "is_need", "is_part", "id_parent", "id_complete",
]

Need fields to leave out of the file. A listed name is dropped from every need object and from the file’s needs_schema, so the schema always describes exactly what the file contains.

An authored list replaces the default rather than adding to it. Names that no need or schema uses are ignored, so listing a field the project does not define is harmless — which is why the default can carry the Sphinx-Needs core-field names that have no ubCode equivalent. Set an empty list to write every field:

[needs]
json_exclude_fields = []

json_remove_defaults

Type: boolean (default: false)

When true, a field whose value equals that field’s default is left out. This shrinks the file considerably on projects where most needs take the defaults. The file records "needs_defaults_removed": true, and a reader must re-apply the defaults published in needs_schema itself.

reproducible_json

Type: boolean (default: false)

Accepted for Sphinx-Needs compatibility; it has no effect in ubCode.

Sphinx-Needs writes a wall-clock created timestamp into needs.json unless this key is set, which makes the file differ on every build. ubCode never writes that timestamp, so its needs.json is already byte-identical across builds of unchanged sources — the behaviour reproducible_json = true asks for is what ubCode always does. The key is accepted so that a configuration shared with Sphinx-Needs does not have to be edited.