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.

table_style

Type: string (default: "datatables")

The style a needtable renders in when it sets no :style: of its own.

datatables is the interactive table: the reader sorts by column, filters, pages, hides columns, and copies or downloads the rows as CSV, all in the browser. table is the static one — a scroll frame with a sticky header row and no client-side behaviour. A needtable that sets its own :style: uses that instead; a :style: naming neither style falls back to this key, as it does in a Sphinx-Needs build.

This is the ubCode spelling of Sphinx-Needs’ needs_table_style, with the same default and the same two values. Nothing called DataTables is involved: the interactive table is ubCode’s and Sphinx-Needs’ own shared asset, and the value keeps its name because it is the one both tools already write.

table_page_size

Type: integer (default: 10)

How many rows an interactive needtable shows per page.

A needtable that sets its own :page_size: uses that instead, and a reader can choose another size from the table’s own control. It must be a positive integer — 0 means “All” in the offered sizes below, not a page size — and an invalid value is reported and this default used. A :style: table table ignores it and shows every row it rendered.

table_page_sizes

Type: list[integer] (default: [10, 25, 50, 0])

The page sizes an interactive needtable’s control offers, where 0 means All.

Must be a non-empty list of non-negative integers. The page size actually in use is added to the list when it is not already there, so the control can always show the size the table is on.

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, and that each node of a .. needflow:: drawn by a PlantUML executable is drawn from ([build.plantuml] needflow). 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. For a needuml 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 — or, when only part of the diagram is affected, needs.uml_partially_converted, which names the lines that are missing. A needflow drawn by a PlantUML executable has no such limit — the executable draws whatever the template produced, with one narrowing: a % or an & in the produced label is escaped, so a template that deliberately writes %version() or %date("yyyy") draws that text literally here where a Sphinx build lets PlantUML’s preprocessor expand it. (The label is a need’s data; a [needs.flow_configs] block is not, and its content is spliced verbatim — that is the one place a PlantUML built-in is honoured.) A needflow drawn as a Mermaid flowchart does not read this key at all: it draws the same three parts (the type title, the wrapped title in bold, the id) in Mermaid’s own label syntax.

[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"

Values may be strings, numbers, booleans, arrays of those, or nested tables. A TOML date-time is reported as a config diagnostic at needs.render_context — quote it as a string (release = "2026-01-01"); one inside an array is a configuration error, like an array of tables or of arrays.

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.

flow_engine

Type: string (default: "")

Added in version 0.36.0.

Which engine draws the project’s needflow diagrams.

[needs]
flow_engine = "plantuml"

plantuml asks for them to be drawn by the PlantUML executable [build.plantuml] names, and graphviz asks for them not to be — ubCode has no Graphviz pathway, so graphviz means “not PlantUML” rather than naming a second engine. Any other value is reported once for the project (config, at needs.flow_engine) and ignored.

This is only the default: [build.plantuml] needflow — or a builder’s own [build.html.plantuml] / [build.pdf.plantuml] — wins over it, and a diagram’s own :engine: wins over both. Drawing with PlantUML also needs [build.plantuml] enabled, since that is what names the executable. Unset (or "") leaves the choice to [build.plantuml] needflow, which is off by default.

This is the ubCode spelling of Sphinx-Needs’ needs_flow_engine, and it is read here so that one ubproject.toml shared by both tools need not say the same thing twice. Where the two halves of that shared file disagree — this key saying graphviz while [build.plantuml] needflow is true, or the reverse — ubCode follows the build key and a Sphinx build of the same file follows this one, so the same project draws its diagrams with different engines in the two tools. Neither tool reports it: each is doing what its own precedence rules say. If you want one answer from both, set only this key and leave [build.plantuml] needflow unset.

[needs.flow_configs]

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

Named blocks of PlantUML configuration that a .. needuml::, .. needarch:: or .. needflow:: 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. The two needflow direction names below are the exception: they are read as a direction rather than looked up here, so they are never reported.

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.

A needflow inserts a named block’s lines only when it is drawn by a PlantUML executable ([build.plantuml] needflow), and then exactly as they are inserted for a needuml. Its :config: is read on both arms for the layout DIRECTION, though: a block containing left to right direction or top to bottom direction sets that direction as the low-precedence source for the diagram’s :direction:, which is how Sphinx-Needs reads it, and a block setting both resolves to top-to-bottom as it does there. Two names also work with no entry behind them: lefttoright and toptobottom are read as the direction itself and are never reported as unknown — Sphinx-Needs ships them as entries of this table and ubCode ships none, so the two spellings are built in instead. A block defined under either name is read like any other block: its content decides the direction, and the name does not override it. Because a block’s content is spliced verbatim, it is also the one place a PlantUML built-in such as %version() is honoured — a % in a need’s title or in a diagram_template’s output is escaped and drawn literally, but configuration you wrote is passed through as you wrote it, exactly as Sphinx-Needs passes it.

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

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_fields’ default / 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.

Derived fields

A derived field is a field, or a link type, whose value a rule computes: the rule is written once in ubproject.toml, applies to every need (or, declared per need type, to the need types it names: see Rules per need type), and no author can set the field. Declare it with a derive table on its [needs.fields.<name>] or [needs.links.<name>] entry (see Fields and Link types), or on a table entry of the older extra_options list or a [[needs.extra_links]] entry: a kind, which says what the rule computes, and that kind’s roles, each under its fixed name. The example declares the fields and link types its rules read first:

[needs.fields.hours]
schema = { type = "number" }

[needs.fields.asil_own]
schema = { type = "string", enum = ["QM", "A", "B", "C", "D"] }

[needs.links.parent]
outgoing = "parent"

[needs.links.tests]
outgoing = "tests"

[needs.links.satisfies]
outgoing = "satisfies"

[needs.fields.owner]
schema = { type = "string" }
derive = { kind = "copy", field = "owner", over = "parent" }       # a lookup, one hop out

[needs.fields.total_hours]
schema = { type = "number" }
derive = { kind = "sum", field = "hours", over = "links" }         # a roll-up over outgoing links

[needs.fields.verified]
schema = { type = "boolean" }
derive = { kind = "all", test = "status == 'passed'", over = "tests_back", where = "type == 'test'" }

[needs.fields.asil]
schema = { type = "string", enum = ["QM", "A", "B", "C", "D"] }
derive = { kind = "max", field = "asil_own", over = "satisfies", transitive = true, include_self = true }

[needs.fields.hash]
schema = { type = "string" }
derive = { kind = "hash", fields = ["title", "content", "status", "links"] }

[needs.fields.fp]
schema = { type = "string" }
derive = { kind = "fingerprint" }                                  # a digest of the whole need

[needs.links.related_open]
derive = { kind = "links", where = "type == 'req' and status == 'open'" }

[needs.links.mentions]
derive = { kind = "content_links" }

ubCode computes every kind as sphinx-needs 9.0.0 does.

The kinds

kind

declared on

required roles

optional roles

the value

copy

a field

field

one of from, over; select (only with over)

the value of field: on this need, on the need from names, or on the targets of over

sum

a field

field, over

where

the sum of field, a number field, over the candidates: number, or integer when field is

count

a field

over

field, where

the number of candidates (with field, of those that set it): integer

min, max

a field

field, over

where, transitive, include_self

the least or greatest value of field, a number field or a string field with an enum (ordered as the enum lists its values, the first least)

any, all

a field

over, and one of field (a boolean field) or test

where

whether some or every candidate holds: boolean; over no candidates any is false and all is true

collect

a field

field, over

where

the distinct values of field over the candidates, in candidate order: an array

hash

a field

fields

—

a SHA-256 of the listed fields of the need, after every other derived field: string

fingerprint

a field

—

exclude

a SHA-256 of the whole need (see The fingerprint), after every other derived field: string

links

a link type

where

include_self, include_parts

the needs passing where, in need-id order (stored character by character, SPC_10 before SPC_9; a needs.json export sorts them naturally)

content_links

a link type

—

from, where

the needs the need’s content references with :need:, in the order written, each once (a part reference as <need>.<part>; a reference naming no need kept as a dead link, except under where)

Every value kind but fingerprint also takes after = "derived", which runs the rule in the late stage, after every other derived field of every need (hash and fingerprint always do). A late rule reads other needs as any value kind does — over a link type, through from — and their late fields too: the late rules run in the order their reads give, so late_total = { kind = "sum", field = "total", over = "links", after = "derived" } sums the targets’ total once it is final, and a late copy of a hash field reads the hash. A dynamic function or a rule without after reading a late field — [[copy("h")]], [[calc_sum("h")]], a filter naming it, { kind = "collect", field = "h", over = "links" } — cannot run before it: the call or the rule is needs.derive_scope and its field keeps its empty value, as in sphinx-needs.

How a value is computed

The rules of the link kinds run first, then the value kinds, then the late stage: a value rule reads every link list final, the backlinks (<link>_back) included. Within a stage a rule runs after every value it reads, and a rule on a cycle of reads is needs.derive_cycle and keeps its empty value.

  • The candidates. over = "<link>" reads the need’s targets in the order they are written, a target written twice read twice, a link to a part read as its need, and a link to no need skipped; over = "<link>_back" reads the needs that link to this one, each once, in id order compared character by character (SPC_10 before SPC_9). where is evaluated on each candidate, then test.

  • Unset values. A candidate whose field is unset (null) is skipped; "" and [] are set. sum adds numbers only (a boolean or a text is skipped); min and max skip a value outside the field’s enum, and keep the first of equal values; collect keeps each distinct value once, a list’s items each.

  • No candidate. sum and count give 0, any false, all true, collect []; min, max and a copy with nothing to copy compute no value, and the field keeps its empty value.

  • A copy over several targets. "first" and "unique" take the value of the id that sorts first character by character among the targets that set the field; "unique" with more than one such target also reports needs.derive_unique (a warning). "list" takes every value, in candidate order, duplicates kept.

  • The type. The value takes the derived field’s type: a count into a number field is 2.0, a sum of integers into an integer field is 7. A computed value the field’s schema refuses is reported as needs.dynamic_function, and the field keeps its empty value.

  • Transitive. min or max over every need reachable through over; a need on a cycle of links reaches itself, so every need of a cycle holds the same value. where filters the values read, not the needs reached. A rule over its OWN field (the multi-level roll-up of Rules per need type) reads the value each reached need computes: a cycle of links among those needs is then a cycle of reads (needs.derive_cycle on each), and so is include_self = true on every need, which reads its own value.

  • The hash. The SHA-256 of the listed fields as one compact JSON array: a link list as its target ids, conditions dropped, each once, in natural order; a number as Python writes it (1e+16, 1.5e-05). ubCode and sphinx-needs write the same hash for the same need.

  • The fingerprint. See The fingerprint.

The roles

field

The field read on each candidate: a field, a link type, <link>_back, or a context field such as id, type, docname or lineno. On copy with neither from nor over, the need’s own field.

fields

The need’s own fields hashed by hash, in this order: fields and link types, never <link>_back.

exclude

fingerprint only: names to leave out of the fingerprint, each one it would include, listed once, such as exclude = ["id"]. An empty list leaves nothing out.

over

A declared link type: its targets are the candidates, in the order they are written, a target written twice counted twice. <link>_back reads the needs that link to this one instead, in id order compared character by character (SPC_10 before SPC_9).

from

The id of one need, read instead of this one. copy reads an external need’s fields as its source states them; content_links cannot read an external need, which has no content: naming one fails the rule, which is reported.

where

A filter narrowing the candidates, evaluated on each candidate. On links, the filter over every need; it must not be empty. On content_links, the filter each referenced need must pass (a part reference through its need). A candidate the filter cannot be evaluated on — one lacking a field it compares, say — does not pass, and nothing is reported.

test

A filter asserted of each candidate, instead of a boolean field (any and all).

select

Which value a copy over several targets takes: "first" (the default) takes the target that sets the field whose id sorts first character by character, "unique" takes the same and reports when several do, "list" takes every value, into an array field.

transitive

min and max only: every need reachable through over, not only its direct targets (a need on a cycle reaches itself).

include_self

min and max with transitive: count the need’s own field too. links: let a need be linked to itself when it passes the filter. Default false.

include_parts

links only: need parts are candidates too, a passing part linked as <need>.<part>. Default false. To where, a part is its need with a few names of its own: id is the part id, id_complete is <need>.<part>, id_parent is the need’s id, is_part is true (is_need false), and content is the part’s own text; it links to nothing (every link type is empty) and has no parent_need. Every other name — type, status, title, your own fields — is its need’s. A need’s parts come right after it, in part-id order, and the need carrying the rule leaves out its own parts with itself, unless include_self. A broad where with include_parts on every need grows every stored list with every part that passes, so keep the filter narrow on a large project.

after

"derived": run in the late stage, after every other derived field of every need.

where and test are written in the filter language sphinx-needs and ubCode both evaluate, on the candidate alone: they may not be blank, name needs or current_need, or read a c. attribute such as c.this_doc(), since a rule applies to every need wherever it is written.

The join role, for a correlated lookup (“every need whose case_name is one of my test_case values”), and the expr kind, for an expression over the need’s fields, are reserved and not available yet. A rule that uses either is refused. types is not a role: a rule for some need types is declared under derive_by_type (Rules per need type).

Rules per need type

A derive_by_type table beside derive (or instead of it) gives a rule per need type: its keys are need types, by the directive name type holds (task, not Task), and each value is a rule as derive takes. A need uses its type’s rule, else derive, else none. It is read wherever derive is: on [needs.fields.<name>] and [needs.links.<name>], and on the older extra_options and [[needs.extra_links]] entries.

The pattern it is made for is a value written on the leaves and rolled up through every level above. Effort is authored on tasks; total_effort copies it on tasks and sums it over the links on stories and epics, through any depth (the three types are declared, since every key must name a need type of the project):

[[needs.types]]
directive = "task"
title = "Task"
prefix = "T_"

[[needs.types]]
directive = "story"
title = "Story"
prefix = "S_"

[[needs.types]]
directive = "epic"
title = "Epic"
prefix = "E_"

[needs.fields.effort]
schema = { type = "number" }

[needs.fields.total_effort]
schema = { type = "number" }
nullable = false
derive_by_type.task = { kind = "copy", field = "effort" }
derive_by_type.story = { kind = "sum", field = "total_effort", over = "links" }
derive_by_type.epic = { kind = "sum", field = "total_effort", over = "links" }
.. epic:: Checkout
   :id: EPIC_1
   :links: STORY_1, TASK_3

.. story:: Pay by card
   :id: STORY_1
   :links: TASK_1, TASK_2

.. task:: Card form
   :id: TASK_1
   :effort: 3

.. task:: Card checks
   :id: TASK_2
   :effort: 5

.. task:: Receipt
   :id: TASK_3
   :effort: 2

The tasks hold 3.0, 5.0 and 2.0, STORY_1 holds 8.0 and EPIC_1 10.0. The field is two fields, effort and total_effort, because a derived field is closed on every need type: the tasks’ own value is written in effort, and total_effort is computed everywhere.

A need of a type without a rule holds the field’s off-type value, for good:

the field declares

a need of a type without a rule holds

a literal default

the default, in the field’s type (default = 0 on a number field is 0.0), checked by the field’s schema as any default is

no default, and is nullable

null

no default, and is not nullable

its empty value ("", 0, 0.0, false, []), which the schema does not judge

a link type with a literal default

those links (default = [] is no links, as no default)

a link type without one

no links

An imported or external need lacking the field holds the off-type value of its type when its type has no rule, and the empty value (null when nullable) otherwise; a value it states is kept. In needs.json, the field’s needs_schema entry is "readOnly": true, and its default is the off-type default (a link type’s as its sorted link list).

What may sit beside it. A literal default beside derive_by_type alone is the off-type value. Beside derive and derive_by_type together a default is refused, as beside derive: every type has a rule. predicates, and a default that is a [[…]] or a variant, beside derive_by_type are refused (config.derive_invalid) and ignored: a value that differs per need is what a rule is for. [needs.global_options] refuses a default for the field, as for any derived field.

When it cannot be read, each entry is reported on its own, at its key (needs.fields.<name>.derive_by_type.<type>):

  • a key naming no need type: the entry is ignored; a key that is a type’s title gets a hint (Requirement is not a need type, did you mean req?);

  • a derive_by_type that is not a table, or an empty one, at needs.fields.<name>.derive_by_type: it adds no rule, and a readable derive beside it still applies on every need type; the field stays derived;

  • an entry that cannot be read, as derive: the needs of that type hold the empty value (not the default, which belongs to the types without a rule); every other type is unaffected;

  • either key on a core field: refused, the field left as it is.

Within a field the findings come in the order derive, derive_by_type, its entries by need type, then a key beside derive_by_type. Rules that read each other’s own values are looked for per need type: a type’s entry on such a cycle cannot be read there, and a derive closing one only through some types’ entries cannot be read on those types, and applies on the others (its finding names them).

Stages are per need. A type’s rule may run late (after = "derived") while another type’s rule of the same field does not: a reader of the field reads each need’s own value, and only a need whose rule runs later than the reader is needs.derive_scope.

Who may set a derived field

Nobody but the rule, on every need type, also the types without a rule (Rules per need type). A directive option naming a derived field or link type, a dynamic function, variant or variant-data reference written in one, and a needextend option naming one are each reported as needs.derive_authored (a warning), and ignored. A derived field or link type takes no default or predicates: each is reported as config.derive_invalid and ignored, and [needs.global_options] refuses a default for one with its own finding. A link type declared copy = true copies its targets into links; when links is itself derived, the copy is not performed, and the links rule reports it as config.derive_invalid. A derived link type cannot be copied in this release: copy = true on one is reported as config.derive_invalid at its rule, and its targets are not copied into links.

A needextend whose filter names a derived field is matched before any rule runs, against the value from before: it is reported as needs.derive_scope (a warning) at the needextend, naming the needs that compute the field (a need whose type has no rule holds its off-type value already, and is not named). Write the filter over the fields the rule reads instead.

When its rule computes no value — fails, cannot be read, or finds nothing to compute from — a derived field holds its empty value, its placeholder, which the field’s own schema does not judge: a non-nullable enum field holding "", or a derived link type with minItems holding [], is not reported. A value a rule computes is validated like any other, 0, "" and [] included.

A rule does not run on an imported or external need (needimport, [[needs.external_needs]]): the field keeps the value its source states. In needs.json, the needs_schema block marks every derived field and link type "readOnly": true, so a project importing it takes the value as data.

When a rule cannot be read

A rule that cannot be read is reported once, as config.derive_invalid at the rule’s key (needs.fields.<name>.derive, needs.fields.<name>.derive_by_type.<type>, or needs.extra_options[<index>].derive on the older list), naming the kind and the offending role: a derive that is not a table, a missing or unknown kind, an unknown role, a role the kind does not take or a required one it lacks, a role whose value has the wrong type, or a blank field, over or from, or an empty fields, an exclude that is not a list of names, names one twice, or names nothing a need has or a name a fingerprint never includes (such as docname), a value kind on a link type or a link kind on a field, a value the field’s schema cannot hold, sum over a field that is not a number, min or max over a field with no order, a where or test that is blank, does not parse or is not portable, an over naming no declared link type, select = "list" on a field that is not an array, and two rules that read each other’s values on the same need (copy of an own field, hash). The field stays derived, closed to authors, and holds its empty value; the configuration still loads, so a project written for a newer kind or role still builds with an older ubCode.

A derive on a core field (id, type, status, tags, title, docname, …, and parent_needs, as a link type or a field) is refused the same way, and ignored: the field stays an ordinary field.

The fingerprint

A fingerprint is the lowercase hex SHA-256 of the UTF-8 text of one JSON object holding the whole need, its keys sorted, its values written as for hash (no whitespace, non-ASCII characters not escaped, numbers as Python writes them, and a number field’s value as a float). ubCode and sphinx-needs write the same fingerprint for the same need.

  • What it includes. id, type, title, status, tags and content (as needs.json holds it: a need nested in another’s body is part of the parent’s content, so editing or moving the nested need changes its parent’s fingerprint too), every field the project declares under [needs.fields] or [needs] extra_options, and every link type, links among them; none of them derived.

  • What it never includes. A derived field or link type (one with derive or derive_by_type, on any need type; so no hash and no other fingerprint), a back link list (<link type>_back), parent_needs, parts (a part’s text is in content), constraints, where the need is (docname, lineno, sections), how it is shown (layout, style, collapse, hide), what is recorded about it (is_external, has_dead_links and the like), and a field ubCode adds for you, such as those of [test_reports] or a [codelinks] URL field, unless the project declares that name itself.

  • When a name enters. Only where the need’s value differs from the field’s default, compared as JSON text: the field’s default, else null for a nullable field (a default that is a [[…]] or a variant is no literal default, and so counts as none), and for a field that is neither, nothing: it always enters. status and tags are compared against theirs (null and [] unless configured), content against "", and id, type and title always enter. A link type, and tags, without a literal default are compared against []. A number field’s value and default are compared, and written, as floats: default = 1 is 1.0. So a value written equal to its default is the same as leaving it unset, while "", false, 0 or [] differ from a null default, and -0.0 from 0.0.

  • Declaring a field. A new field or link type moves no fingerprint while the needs leave it unset, if its default is a literal or there is none. A default that is a [[…]] or a variant, and predicates, give the needs a value, which enters as a written one would: adding owner with default = "[[copy('title')]]" moves the fingerprint of every need, and adding severity with predicates = [["type == 'spec'", "major"]] that of every spec. Changing a field’s default moves the fingerprints of the needs that wrote the old or the new default out, and not of those that rely on it, although their value changes.

  • Links. A link list is written as for hash, and compared with its default the same way: the ids it links to, without their conditions, sorted and without duplicates. So a condition pinning another need’s fingerprint never moves the fingerprint of the need holding it, while linking another need does.

  • Leaving names out. exclude leaves names out, such as exclude = ["id"], under which two needs with the same contents have the same fingerprint.

  • When it is computed. After every other rule, from the need’s final values (after every needextend, variant and [[…]]); it reads no derived field, so two fingerprints with the same exclude hold the same value. It does not run on an imported or external need.

  • Frozen. No later release changes which names a fingerprint includes, when a name enters, or how the text is written: a fingerprint written down stays current until the need, or the configuration of the fields it reads, changes.

For a need REQ_1 of type req, titled Größe, with status open and no other value set, the text is {"id":"REQ_1","status":"open","title":"Größe","type":"req"}.

This is not the fingerprint ubc agent records with a review verdict (Reviews and quality analysis), which also covers the needs a need is trace-linked to, and ignores whitespace.

Detecting a changed need. A fingerprint changes whenever its need does, so a link condition naming the fingerprint a need had when it was reviewed reports the need once it changes:

.. spec:: Reviewed against REQ_1
   :id: SPEC_1
   :links: REQ_1[fp == "3f9a1c2b7d4e0a8c6f1e5d2b9a7c4e3f8d6b1a0c9e7f5d3b2a4c6e8f0a1b3c5d"]

While REQ_1 is unchanged, the condition holds. Once its title, content, status or any other value it includes changes, its fingerprint does, and the build reports needs.link_condition_failed at SPEC_1; writing the new fingerprint into the condition accepts the change. Writing only the start of the fingerprint is enough to tell a change apart: REQ_1[fp.startswith('3f9a1c2b7d4e')].

Derived fields and sphinx-needs

sphinx-needs reads derive and derive_by_type from its 9.0.0 release, with the same kinds, roles and findings (the configuration finding is needs.derive_invalid there). An earlier sphinx-needs ignores the key without a word, so a Sphinx build leaves the field authorable and unset. ubCode cannot see which sphinx-needs a project builds with, so a project that declares derive or derive_by_type gets one config.derive_sphinx_minimum notice, graded info, naming the release, at the first key of the first derived field by name (or link type, when no field has one); ignore = ["config.derive_sphinx_minimum"] in [lint] silences it.

Several kinds have an inline spelling, a dynamic function written in one need’s option, with the same meaning:

dynamic function

the kind it spells

copy(field, need_id)

copy with from

calc_sum(field, filter, links_only=True)

sum over links, filter as where

check_linked_values(result, field, values, filter_string, one_hit)

all (any with one_hit) over links, filter_string as where

links_from_content(need_id, filter)

content_links with from and where

links_from_filter(filter, include_self)

links

One difference between the inline calls and the kinds is kept, as sphinx-needs keeps it: a need the filter of calc_sum, check_linked_values or copy cannot be evaluated on (one lacking a field the filter compares) is still counted by calc_sum, still tested by check_linked_values and dropped by copy, each reported once per call as needs.filter, and it fails links_from_content; a kind’s where, and links_from_filter, let such a need not pass, silently.

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, as written, and to the source’s list-valued links that name one of its own needs (a link written as a string, or to a part, keeps its target as written)

  • 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

Each need is named by the key it is filed under in the source’s needs.json. A need whose own "id" differs from that key is still read under the key, and reported once (needs.external), naming both.

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: open
   :phase: <<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: open
   :phase: <<[status == 'open']: active, inactive>>

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

A variant cannot read the field it fills: :status: <<[status == 'open']: active, inactive>> asks for status to decide status, which is a cycle (needs.derive_cycle), and the field keeps its empty value. Put the variant on another field, as above; see When a call reads a value another call computes.

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 fields — content, docname, lineno, sections, section_name, is_external, is_directive, is_import, is_src_trace, is_modified, modifications, is_need, is_part, id_parent, id_complete — 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. It lives in one of two places in ubproject.toml: the shared [variants] table, which every tool reading the file uses, or the legacy variant_data and variant_data_file keys of [needs], which ubCode still reads, without a warning.

Note

sphinx-needs reads [variants] from 9.0.0 on, and sphinx-mounts from 0.3.0 on; every earlier release ignores the table silently, so keep the keys in [needs] until your Sphinx build uses those. The examples on this page and in Variant builds use [needs] for that reason; the same values work unchanged under [variants].

variant_data

Type: object (default: unset)

An inline, nested table of variant data, declared directly in ubproject.toml under [needs.variant_data] (as shown above). Its [variants] spelling is data. Setting variant_data = {} counts as setting the table (see below).

variant_data_file

Type: string (default: unset)

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. Its [variants] spelling is data_file, and it follows the same rules: one non-empty path, joined as written.

[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.

The [variants] table

[variants] holds the same two values under the names the whole tool family shares, so one ubproject.toml gives sphinx-needs, sphinx-mounts and ubCode the same variant data:

[variants]
data_file = "variant_data.json"

[variants.data]
platform = "windows"
data

Type: object (default: unset)

An inline, nested table of variant data, under [variants.data], merged over the file exactly as [needs.variant_data] is. Setting data = {} counts as setting the table (see below).

data_file

Type: string (default: unset)

Path to a JSON file holding one object of variant data. It is one non-empty path — a list of paths, and an empty string, are refused. A relative path is joined to the directory of the ubproject.toml that sets it, as written: a .. in it is kept, not folded away.

Any other key under [variants] is ignored and reported as config.variants_unknown_key, so one file can serve tool versions that read more keys. A variants that is not a table, or a data or data_file of the wrong type, is a configuration error.

Which table is read

The variant data is read from one table, never one key from each:

  • [variants], when it sets data or data_file — data = {} included; an empty [variants] table, or one holding only other keys, sets neither;

  • otherwise the legacy [needs] keys;

  • otherwise nothing, and var.* is empty.

When both tables set variant data, [variants] is read and each [needs] key it ignores is reported as config.variant_data_location, at that key (needs.variant_data or needs.variant_data_file). The ignored data file is never opened, so a missing or broken one there is not reported.

Across extend and -c, the inline table is merged key by key: the nearer configuration’s values (a -c override is the nearest) are deep-merged over the inherited ones, exactly as the inline table is merged over the file, and a nearer data file replaces an inherited one. The table is chosen once, after that merge. [variants] wins wherever the chain or an override declares it: an override or configuration that sets [variants] data or data_file switches a [needs] project to it, and one that sets [needs] under a [variants] declaration is ignored — both warned. So migrating a shared base moves every child still on [needs] to the base’s table, and each such child’s own [needs] values are dropped (warned at its keys). Conversely, a project on [variants] that extends a base still on [needs] reads its own [variants] alone and reports the base’s [needs] keys: migrate the base first, or, while a shared base is still on [needs], add config.variant_data_location to the child’s lint.ignore (which replaces an inherited list — repeat the base’s entries), or pass it for one run (ubc check --extend-ignore, ubc build html --lint-extend-ignore). An unknown [variants] key in a base is reported in every configuration that extends it.

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.

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

A mixed-type array, and a TOML date-time value (which is not a variant value), are reported as config.variant_data_invalid at the inline table’s key; a date-time inside an array is a configuration error, like an array of tables or of arrays in ubproject.toml. The data file is checked on its own, before the merge, so an inline value cannot hide an invalid file value by replacing it. Everything wrong with the file — missing, not JSON, not one object, a JSON null, or a value of a shape above — is reported at the data-file key, under the same code, as is an empty file name. Each is a warning, so ubc check and ubc build html exit non-zero, and a bare "config" in lint.ignore does not silence it (see Code matching and .* wildcards).

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 seven 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, filter=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 field — id, 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, id_parent, id_complete. 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. With filter, it copies from the need passing the filter whose id sorts first character by character, and falls back to the need need_id names, or to the current need, when none passes

  • links_from_content(need_id=None, filter=None) — returns the needs referenced via :need: roles in a need’s own content, the current need’s unless need_id names another (a need_id naming no need, or an external need, which has no content, fails the call, which is reported), keeping with filter only the needs that pass it. need_id may be given as a keyword or as the first positional argument, filter as the second

  • check_linked_values(result, search_option, search_value, filter_string=None, one_hit=False) — compares the search_option field of every need this one :links: to (with filter_string, of those passing it) 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. A link to no need is skipped (the dead link is reported on its own). It fails only 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", filter=None, 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 (a link to no need skipped), counting with filter only the needs that pass it

  • links_from_filter(filter, include_self=False, include_parts=False) — links every need passing the filter, in need-id order (stored character by character, SPC_10 before SPC_9; a needs.json export sorts them naturally), leaving the current need out unless include_self is set; an empty filter is an error. include_parts=True tests every need’s parts too, as the links kind’s include_parts does (also when a need.<attr> gives it)

Several of them are the inline spelling of a derived-field kind, which writes the same rule once for every need.

A filter (filter, filter_string on check_linked_values) is written in the filter language and evaluated on each candidate need. An empty filter ("") is no filter; one that is only whitespace is a filter that cannot be evaluated on any need, and each call treats it as it treats such a need: calc_sum still counts every need and check_linked_values still tests every linked need (each reporting needs.filter), copy and links_from_content fail the call, and links_from_filter refuses it. A filter naming needs or current_need, or reading a c. attribute such as c.this_doc(), reads beyond the candidate need, and is not evaluated here (the portable form of a test over other needs is a boolean field derived with kind = "any", filtered on in a sum rule’s where = "<flag>" or with calc_sum(…, filter="<flag>")). A call with such a filter is reported where it is written (needs.dynamic_functions, info), 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.

A call in a need’s field that runs but returns a value of the wrong type for that field — a number, an integer, a boolean or a list in a string field, or a string or a list in an integer, number or boolean one — is a warning on the need, as in a Sphinx build, and the field keeps its empty value (a list keeps the items written beside the call): needs.dynamic_function, Dynamic function value type mismatch for field 'summary' on need 'REQ_001': expected string, got number. An integer is accepted in a number field. An array field takes a value of its item type or a list of such values, and stores an empty list as []; one item of another type fails the whole field, the values written beside the call included (… expected array(integer), got string). To store such a result, declare the field with its type, for example { name = "total", schema = { type = "number" } } under extra_options. A call that returns nothing (None) is the same warning, … got null, in a field that may not be empty — an extra_options entry without a schema is one — and leaves a field that may be empty (nullable) at null.

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.

When a call reads a value another call computes

A call may read a value another call computes: copy('summary', 'B') where B’s summary is itself a [[...]] or a <<...>> variant, a calc_sum over such values, a calc_sum(..., links_only=True) or check_linked_values whose own links carry a call, or a variant condition naming one. ubCode resolves the calls in dependency order: first every call in a link type, such as links, then every other call, each after every value it reads. So the reader gets the computed value, on every build, whatever order the needs and documents are written in. A call in a link type that reads through its need’s own links, when those are computed too, runs after them, and after every need that computes the value it reads there. Between the two steps the backlinks are built, so a call or a variant condition can read them as <link type>_back: copy('links_back') lists the needs whose links name this one, sorted by id, and is an empty list when there are none — a list either way, so it belongs in an array field; <<['REQ_1' in links_back]:linked, unlinked>> tests it, and len(links_back) == 0 holds when none links here. The dead-link flags are set at the same point: has_dead_links, whether a link of the need names a need that does not exist or a part its need does not have, and has_forbidden_dead_links, the same over the link types that do not set allow_dead_links.

Two kinds of read cannot be ordered, and each is a warning on the need that makes it:

needs.derive_cycle

The value is on a cycle of reads: A’s summary copies B’s, which copies A’s; :hours: [[calc_sum('hours')]], which sums every need’s hours, its own included; or a variant whose condition reads the field it fills, :status: <<[status == 'open']: active, inactive>>. Every value on the cycle keeps its empty value (a list keeps the items written beside its calls), and each gets one finding naming the cycle. Break the cycle: read another field, or give one of the values an authored value. A call that reads a value on a cycle reads that empty value and gets no finding of its own.

needs.derive_scope

The read needs a value that is not final where it is made: a call in a link type reading a field another call computes, or a backlink, both of which are computed after the links (through the need’s own links, when those are computed too, a field that any need computes); a call, or a rule without after = "derived", reading a field a hash rule or a value kind with after = "derived" computes, which runs after every other field; a need.<attr> argument naming which need or field a call reads, where that attribute is itself a call (one the need does not set at all — an unset untyped extra option is the empty string — fails the call instead, needs.dynamic_function, need.src selects what the call reads, and is not set); or a needextend filter naming a field that a call or variant computes on some need, since every needextend applies before any call resolves. The call is not run and its field keeps its empty value (a link or an array keeps the items written beside the call); a needextend still applies, matched against the values as written. Read a value that is final before the read: an authored field, or, for a needextend, write the condition as a variant or a call on the field the extend would set.

Both fail ubc check and the build gate, as warnings do, and can be silenced like any other code:

[lint]
ignore = ["needs.derive_cycle", "needs.derive_scope"]

A silenced finding still leaves the value empty.

Earlier releases resolved every call in one pass: a call read the value stored before resolution — the field’s empty value, or only the literal part of a list — and the read was reported as needs.derive_unresolved, an informational finding. That code is retired; an ignore that still names it is harmless. Released versions of Sphinx-Needs answer these reads by document order, and adopt the same dependency order in 9.0.0; see Dynamic functions resolve all seven built-ins, and their filters.

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.