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
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. Iffalse, 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_requiredistrue.- 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]defaultorpredicatesrow 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 asclean— 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: cleanstops being a no-op and becomes a deliberate override: under the example above, a need with no:layout:drawscompleteand a need with:layout: cleandrawsclean.This is the ubCode spelling of Sphinx-Needs’
needs_default_layout, and it is the same key in the same table:needs_from_tomlmaps[needs] default_layoutontoneeds_default_layout, so aubproject.tomlshared betweensphinx-buildandubc build htmlnow 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,needflowandneedsequence).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. Set0for no cap — from either source. The cap applies after the filter and the sort, in both the editor’s rendered preview andubc build html. A build also emits abuild.need_view_truncatedwarning for each truncated view, so a capped view is visible in the build log without opening the built page.- part_prefix
Type:
string(default:"→ "— a right arrow and a non-breaking space)The text put in front of a need part’s ID and content in a
needtablethat 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 inubc build htmlalike, 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 holdingubproject.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 noubproject.tomlat 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
typesone entry per configured need type, in declaration order, with the keys
directive,title,prefix,colorandstylelinksone entry per configured link type, with the keys
option,incoming,outgoing,copyandallow_dead_linksoptionsthe project’s extra field names
usageneeds_amountandneeds_types(a count per configured type)report_directivethe string
dropdownA 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 —
.needor.txt, not.rstor.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()ands.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_projectorneeds.report_template_failed.- flow_direction
Type:
string(default:"down")The default layout direction for
needflowdiagrams — where the graph grows.One of
down,up,rightorleft, or the equivalent engine tokensTB/TD,BT,LRandRL. Aneedflowthat sets its own:direction:uses that instead. An empty string is the same as unset.- flow_show_links
Type:
stringorboolean(default:"none")The default edge labelling for
needflowdiagrams.outgoingandincominglabel each edge with the link type’s corresponding display name,typelabels it with the link type’s own name, andnoneleaves edges unlabelled.truemeansoutgoingandfalsemeansnone, so a value carried over from Sphinx-Needs’needs_flow_show_linksmeans the same thing here. Aneedflowthat sets its own:show_link_names:uses that instead, including:show_link_names: noneto 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
needflowgets when its:show_legend:names none.The value is a key of
flow_legendsbelow. 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. Aneedflowwhose: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 byflow_show_legendabove.[needs.flow_legends.wide] parts = ["types", "links"] placement = "external"
partsThe sections the legend lists, as a list, in the order they are drawn:
types(each drawn need type with its colour) andlinks(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.
placementWhere the legend is drawn:
internal(inside the diagram itself) orexternal(a table beside it). Unset takes the diagram engine’s default placement, which for the Mermaid flowcharts ubCode draws isexternal.internalis 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 whoseubproject.tomlis shared with a Sphinx-Needs build, whereinternalis 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
partsis 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’sflow()renders each need with. Unset — or set to""— uses ubCode’s built-in template, which is byte-identical to the Sphinx-Needs default.[needs] diagram_template = """{{ need.type_name }} {{ need.id }}"""
The template produces PlantUML, is given the need as
need, and sees every name [needs.render_context] adds.This is the ubCode spelling of Sphinx-Needs’
needs_diagram_template, with the same default. ubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, so a template that emits a PlantUML construct Mermaid has no equivalent for is reported (needs.uml_unconvertible) rather than drawn.
- [needs.render_context]
Type:
tableofstring→ value (default: empty)Extra names every template ubCode executes can read — a
needumlorneedarchbody, diagram_template, and theneedreportreport_template.[needs.render_context] product = "ubCode" release = "2026.1"
Names here do not reach two template surfaces, deliberately: [needs.string_links] templates and
needs_role_need_template. Both see their pattern’s named capture groups and nothing else, which is also what keeps a project-wide name from silently shadowing a capture group calledvalue.This is the ubCode spelling of Sphinx-Needs’
needs_render_context.
- [needs.flow_configs]
Type:
tableofstring→string(default: empty)Named blocks of PlantUML configuration that a
.. needuml::or.. needarch::pulls in with its:config:option. Each value is a block of PlantUML lines; the option names one or more blocks, comma-separated, and their lines are prepended to the produced diagram.[needs.flow_configs] monochrome = """ skinparam monochrome true skinparam shadowing false"""
.. needuml:: :config: monochrome {{ flow('R_LOGIN') }}
A
:config:naming a block this table does not define is reported (needs.uml_config_unknown) and contributes nothing; a Sphinx build is silent about it, which leaves a diagram that renders perfectly and merely looks wrong.Because ubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, a block’s lines reach the diagram but only the ones Mermaid has an equivalent for change what is drawn —
skinparamin particular is dropped. Adding:debug:to the directive shows the produced PlantUML with the block’s lines in place, which is the way to check that a name resolved.This is the ubCode spelling of Sphinx-Needs’
needs_flow_configs, with one scope difference: there the key is also read byneedflow, and here it is not, because ubCode’s flow engine is not PlantUML.
Need types¶
The types array defines the available need directives in your project:
[[needs.types]]
directive = "requirement" # The RST directive name
title = "Requirement" # Human-readable title
prefix = "REQ_" # Prefix for auto-generated IDs
color = "#BFD8D2" # Color for diagrams and the need-card accent
style = "node" # Style for diagrams
Each type supports:
directive(required): The reStructuredText directive nametitle(required): Human-readable title for the need typeprefix(required): Prefix used for auto-generated need IDscolor(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*_borderstyle frames all four edges and takes that one; on cards that show a type badge, the badge outline still carries the type colour (thefocuslayout 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,#RRGGBBand#RRGGBBAAvalues; 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#000000default means no accent rather than black. So do#000,#000000FFand any fully transparent value (an00alpha, e.g.#00000000), since an invisible accent is not one. A partly transparent colour such as#00000080is 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
colorAccents a card?
req#BFD8D2no — this exact pair is the shipped default
spec#FEDCD2no
impl#DF744Ano
test#DCB239no
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.
Link types¶
Links connect needs to one another and express relationships like implements, tests, or depends on. ubCode supports two configuration formats: the dict-based format (sphinx-needs ≥ 6.3, recommended) and the list-based format (sphinx-needs < 6.3).
Note
Three names cannot be shadowed by a link type:
id and type are refused outright,
and content — the need’s own body — is reported as
config.field_shadowed_by_identity.
A content link is still created and its targets still validated,
but every filter, table, variant condition, report template
and needs.json key answers the body for that name,
so the link is invisible to every one of them.
See Variants for the shadowing rule in full.
Dict-based format (recommended)¶
Note
The dict-based [needs.links] format requires sphinx-needs ≥ 6.3.
It became the recommended format in sphinx-needs 7.0,
where the older list-based needs_extra_links was formally deprecated.
Use [needs.links.<name>] to define each link type as a named TOML table:
[needs.links.implements]
outgoing = "implements"
incoming = "implemented by"
copy = false
color = "#0000FF"
allow_dead_links = false
[needs.links.tests]
outgoing = "tests"
incoming = "tested by"
copy = true
Each link supports:
outgoing(optional): Description for outgoing linksincoming(optional): Description for incoming linkscopy(optional, default:false): Whether to copy to common linksallow_dead_links(optional, default:false): Allow links to non-existent needscolor(optional, default:"#000000"): Color for diagramsstyle(optional, default:"solid"): Line style for diagramsstyle_part(optional, default:"dotted"): Style for partial linksstyle_start(optional, default:"-"): Start marker for diagramsstyle_end(optional, default:">"): End marker for diagramsparse_variants(optional): Whether to parse variant functions (<<...>>) and variant data references (<{ ... }>, see Variant data) in this link’s values. sphinx-needs ≥ 7.0.parse_dynamic_functions(optional): Whether to parse dynamic functions in this link’s values. sphinx-needs ≥ 6.3.parse_conditions(optional, default:true): Whether bracket syntax in link values is interpreted as a condition expression. sphinx-needs ≥ 8.0.description(optional): Human-readable description of the link type. sphinx-needs ≥ 6.3.schema(optional): JSON Schema validation for link values
List-based format (sphinx-needs < 6.3)¶
Deprecated since version 0.25.0: The list-based [[needs.extra_links]] format
(needs_extra_links in sphinx-needs)
was deprecated in sphinx-needs 6.3 in favour of the dict-based format,
and formally migrated in sphinx-needs 7.0.
It remains supported for backward compatibility.
For new projects, prefer the dict-based [needs.links] format.
[[needs.extra_links]]
option = "implements" # The option name used in need directives
incoming = "implemented by" # Description for incoming links
outgoing = "implements" # Description for outgoing links
copy = false # Whether to copy to common links
color = "#0000FF" # Color for diagrams
style = "solid" # Line style for diagrams
style_part = "dotted" # Style for partial links
style_start = "-" # Start marker for diagrams
style_end = ">" # End marker for diagrams
allow_dead_links = false # Allow links to non-existent needs
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 descriptiondefault(optional): Default value if not specified on a needschema(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:
a
:layout:written on the need itself wins over everything;then a
[needs.fields.layout]predicatesrow, then itsdefault— both write a value onto the need, which is why they beat the key below;then [needs] default_layout, which catches only the needs that end up with no value at all;
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.
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 filejson_url: URL to a remote JSON fileid_prefix: Prefix prepended to external need IDsbase_url: Base URL for linking back to the source projecttarget_url: URL template for individual need links (supports{{ id }}placeholder)css_class: CSS class applied to external needsversion: Version string for the external needs sourceskip_in_ubcode(default:false): Whentrue, skip loading these external needs in ubCode analysis
Remote sources¶
The [needs.remote] section controls how ubCode fetches needs.json over
HTTP(S).
It applies to every remote source: [[needs.external_needs]] entries with a
json_url, and needimport directives whose argument is a URL.
Local sources — a json_path, or a file:// URL, which is read straight
from disk — are unaffected by every key here.
[needs.remote]
cache_days = 1
timeout = 10.0
offline = false
cache_days(default:1): How long, in days, an already-loaded remote source’s needs are reused before ubCode re-checks the source.0re-checks on every pass, and a negative value never re-checks. A re-check sends theETag/Last-Modifiedof 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): Whentrue, never fetch remote sources. Each one keeps the needs it was last loaded with and is reported as skipped — useful in network-restricted CI, or for working offline against an index built while online.
A remote source whose fetch fails keeps the needs it loaded last time, rather
than losing them until the next success, and the failure is reported.
Within one long-running process — the language server, or a watch loop — an
unreachable host is not re-attempted for five minutes, so an editing session
against a source that is down does not pay a connection timeout on every pass.
That backoff is per-process and is not persisted, so a one-shot ubc build or
ubc check against an unreachable host waits for timeout on every run;
lower timeout, or set offline = true, if that is the situation you are
in.
Variants¶
The [needs.variants] section defines named filter expressions
that can be referenced from need fields using the <<name: value, fallback>> syntax.
This implements the Sphinx-Needs
variant functions
feature.
[needs.variants]
is_open = "status == 'open'"
is_html = "'html' in build_tags"
is_production = "environment == 'production'"
With these variants defined, need fields can use them:
.. req:: My requirement
:id: REQ_001
:status: <<is_open: active, inactive>>
:extra: <<is_html: web_only, general>>
You can also use inline bracketed expressions without pre-defining a variant:
.. req:: Another requirement
:id: REQ_002
:status: <<[status == 'open']: active, inactive>>
The last comma-separated value is always the fallback when no expression matches.
A variant expression resolves the need’s own fields and links,
the project’s filter_data keys, the build_tags of the current build,
and the context fields —
content, docname, lineno, sections, section_name,
is_external, is_directive, is_import, is_src_trace,
is_modified, modifications, is_need, is_part —
the same names a :filter: expression resolves:
.. req:: Section-aware requirement
:id: REQ_003
:status: <<[section_name == 'Interfaces']: interface, internal>>
A project-declared field or link of the same name wins over a context field,
except for id, type and content, which cannot be shadowed.
Inside a variant expression is_modified and modifications count
needextend matches only —
a need’s own variant resolution cannot be an input to itself.
A name may also carry the n. prefix (<<[n.status == 'open']: a, b>>),
matching what a :filter: expression accepts;
any other namespace is reported as an unknown name.
Note
Variant syntax is only parsed for fields that opt in.
In sphinx-needs ≥ 7.0, set parse_variants = true
on individual fields in [needs.fields].
For older versions, use the deprecated variant_options list
(see Deprecated needs options).
Parse and evaluation errors are surfaced as needs.variant diagnostics,
including a name the expression cannot resolve —
which is reported with a “did you mean” suggestion,
while the expression still resolves to its fallback value.
See also
Variant data for the related var.* namespace
and the <{ ... }> reference syntax, which inject build-wide data
rather than selecting between values conditionally.
Variant data¶
sphinx-needs ≥ 8.2.
Variant data is a nested, read-only key-value store that ubCode exposes
under the var.* namespace inside filter expressions.
It lets a single “150%” model —
one set of needs that describes every product variant —
be filtered, extended, and rendered differently for each build,
without duplicating content.
See also
Variant builds walks through the end-to-end workflow — including building and diffing each variant from the command line. This section is the configuration reference for the options it uses.
For example, a project that targets several CPU architectures and build configurations can keep one copy of its requirements and select the data relevant to the current build:
[needs.variant_data]
platform = "arm"
debug = false
opt_level = 2
archs = ["arm", "x86"]
[needs.variant_data.build]
compiler = "clang"
features = ["networking", "logging"]
With this configuration the following values are available in any filter expression:
var.platform == "arm" # scalar string
var.opt_level == 2 # scalar integer
var.debug == False # scalar boolean
"x86" in var.archs # membership in an array
var.build.compiler == "clang" # nested scalar
"networking" in var.build.features # membership in a nested array
Note
var.* resolves only against the configured variant data.
Unlike need fields or filter_data keys,
it does not depend on the need currently being evaluated,
which makes it suitable for global, build-wide switches.
Looking up an unknown key (e.g. var.missing) is a
needs.variant diagnostic, not a silent false.
Defining variant data¶
Variant data can be provided inline, from a JSON file, or both.
- variant_data
Type:
object(default:{})An inline, nested table of variant data, declared directly in
ubproject.tomlunder[needs.variant_data](as shown above).- variant_data_file
Type:
string(default:"")Path to a JSON file containing variant data, resolved relative to the
ubproject.tomlthat declares it. This is convenient when the values are generated by a build system or shared across several projects.[needs] variant_data_file = "variant_data.json"
{ "platform": "linux", "build": {"compiler": "gcc", "features": ["networking"]} }
When both are set, the file is loaded first as the base,
and the inline [needs.variant_data] values are deep-merged on top,
with the inline values winning:
At nested maps, keys are merged recursively.
At scalars and arrays, the inline value replaces the file value entirely.
[needs]
# file sets platform = "linux", build.compiler = "gcc", build.features = [...]
variant_data_file = "variant_data.json"
[needs.variant_data]
platform = "windows" # overrides the file's scalar value
[needs.variant_data.build]
compiler = "clang" # overrides build.compiler;
# build.features is preserved from the file
Because inline values take precedence, you can keep shared defaults in the file under version control and override individual switches per build.
Value types¶
Variant data values may be:
Scalars — string, integer, number (float), or boolean.
Uniform arrays of scalars — every element must share the same type (mixed-type arrays are reported as a
needs.variantdiagnostic).Nested maps — arbitrarily deep tables, accessed with further dots (
var.build.sanitizers.asan).
Using var.* in filters¶
The var.* namespace is available anywhere ubCode evaluates a
filter expression against your needs configuration.
Variant functions — select a field value per build (see Variants):
.. req:: Bootloader
:id: REQ_001
:status: <<[var.platform == "windows"]: active, inactive>>
Conditional defaults — vary a field’s default via predicates:
[needs.fields.compiler_note]
default = "unknown-compiler"
predicates = [
['var.build.compiler == "clang"', "using-clang"],
['var.build.compiler == "gcc"', "using-gcc"],
]
needextend — extend only the needs relevant to the current build:
.. needextend:: var.platform == "windows"
:status: supported
needimport — import only the needs that match:
.. needimport:: external.json
:filter: var.platform == "windows"
External needs — filter needs loaded from another project:
[[needs.external_needs]]
json_path = "imported.json"
filter = 'var.platform == "windows"'
Referencing variant data in field values (<{ ... }>)¶
In addition to filtering, variant data can be injected directly into a
field or link value using the <{ ... }> syntax.
The text between the delimiters is a var.* path,
and ubCode substitutes the stored value at index time.
Unlike variant functions (<<...>>),
which choose between candidate values using conditional logic,
a <{ ... }> reference is a direct lookup —
there is no condition, just a substitution.
This syntax is only parsed for fields and links that opt in with
parse_variants = true (the same flag that enables <<...>>):
[needs.variant_data]
platform = "arm"
links = ["REQ_001", "REQ_002"]
[needs.variant_data.build]
opt_level = 2
[needs.fields.arch]
schema = {type = "string"}
parse_variants = true
[needs.fields.opt]
schema = {type = "integer"}
parse_variants = true
[needs.links.derived_from]
parse_variants = true
The references can then be used on any need:
.. req:: Scalar reference
:id: REQ_001
:arch: <{ var.platform }>
:opt: <{ var.build.opt_level }>
.. req:: Embedded in a string
:id: REQ_002
:arch: built for <{ var.platform }>
.. req:: Within an array and a link field
:id: REQ_003
:tags: a, <{ var.platform }>, c
:derived_from: <{ var.links }>
A few rules apply:
For string fields the reference may be embedded inside surrounding text; for non-string scalar fields (integer, number, boolean) the reference must be the entire value.
The resolved value is type-checked against the field’s schema.
If the field does not set
parse_variants = true, the<{ ... }>text is kept literally.
The following situations are reported as needs.variant diagnostics
(and leave the value unset):
Invalid expression — the content is not a dotted
var.*path, e.g.<{ platform }>(missing thevar.root).Unknown key — the path does not exist in the variant data, e.g.
<{ var.nonexistent }>or<{ var.build.missing }>.Non-leaf value — the path resolves to a nested map rather than a scalar or array, e.g.
<{ var.build }>.Type mismatch — the resolved value’s type does not match the field, e.g. injecting a string into an integer field.
See also
The corresponding sphinx-needs feature is documented under variant data.
Dynamic functions¶
ubCode resolves the built-in dynamic functions using the [[...]] syntax
from sphinx-needs.
All six built-in functions are supported:
echo("text")— returns the given string unchangedtest(*args, **kwargs)— returns a debug string with the need ID and argumentscopy("field", need_id=None)— copies a field value (also supportslowerandupperkwargs).fieldmay 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. A project-declared field of the same name wins, except forid,typeandcontent.need_id— as a keyword or as the second positional argument — copies from that need instead of the current onelinks_from_content(need_id=None)— returns the needs referenced via:need:roles in a need’s own content, the current need’s unlessneed_idnames another.need_idmay be given as a keyword or as the single positional argumentcheck_linked_values(result, search_option, search_value, one_hit=False)— compares thesearch_optionfield of every need this one:links:to againstsearch_value. Withoutone_hit,resultis returned only when they all match — and also when the need links to nothing at all. Withone_hit,resultis returned whatever the values are: that is a quirk of Sphinx-Needs itself, mirrored here deliberately so the two tools agree. Otherwise the call yields nothing. It fails only on a broken link, or on asearch_optionthat 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 valuecalc_sum("option", links_only=False)— sums a numeric field across the project’s needs, or across only the needs this one links to whenlinks_onlyis set
The filter argument (filter_string on check_linked_values)
is not yet supported.
A call that passes one is reported where it is written,
and what you see in its place depends on where you wrote it:
a role keeps rendering as the role chip — its own text, unevaluated;
a [[...]] written in a needtable’s :style_row: renders ??;
and a [[...]] written in a need’s field or link value
is skipped silently, leaving the field at its other values.
(A bare [[...]] in a need’s body content is not evaluated at all —
use the ndf role there.)
Functions a project registers through needs_functions are not resolved.
For example:
.. req:: My requirement
:id: REQ_001
:status: [[copy("priority")]]
.. spec:: My specification
:id: SPEC_001
:links: [[links_from_content()]]
This specification realises :need:`REQ_001`.
Note
In a need’s own field or link value,
copy('is_modified') and copy('modifications') count needextend matches only —
the same caveat the Variants section carries,
and for the same reason: a need’s own resolution cannot be an input to itself.
Every other context field resolves as a :filter: resolves it.
In prose: the ndf and need_func roles¶
The same functions can be called from inside prose, and the result is rendered in place as plain text:
A dynamic function :ndf:`echo("in a paragraph")` reads inline.
.. req:: My requirement
:id: REQ_002
This need's own id is :ndf:`copy("id")`.
ndf takes the bare call.
need_func takes text containing a [[...]] call
and substitutes the result into that text, keeping what surrounds it:
:need_func:`before [[echo("x")]] after`
renders before x after.
Upstream deprecated need_func in favour of ndf in sphinx-needs 3.1.0;
ubCode resolves both.
Which need a call reads depends on where it is written.
Inside a need’s body, the call reads that need,
so copy("id") above renders REQ_002.
Outside any need there is no current need,
so a function that needs one — copy and links_from_content — renders ??.
A Sphinx build renders ?? there too, but it also logs a warning saying which
function failed and why; ubCode renders the ?? silently.
That difference is deliberate and is recorded under
When ubCode cannot resolve a call.
echo and test need no need and resolve anywhere.
MyST works the same way, with the role spelled {ndf} / {need_func}.
Note
A need_func whose text contains no [[...]] at all
renders that text literally and reports nothing —
which is what a Sphinx build does with it too.
When ubCode cannot resolve a call¶
A call that cannot resolve for any need is reported once,
at the place it is written,
under the needs.dynamic_functions code.
Three things trigger it:
the text does not parse as a function call,
it names a function ubCode does not implement,
or it names one that cannot be called that way.
The report covers need field and link values,
a needtable :style_row:,
and the two roles above.
These findings are informational, not warnings,
and that is deliberate:
ubCode implements every built-in,
but a Sphinx project also has everything needs_functions registers
and ubCode cannot run project Python,
so an unresolvable call is often a statement about ubCode
rather than about the document.
They are reported, they do not fail ubc check,
and they can be silenced like any other code:
[lint]
ignore = ["needs.dynamic_functions"]
A role whose call is reported this way keeps rendering
as the role chip — its own text, unevaluated —
rather than as ??.
?? would claim the call ran and failed,
which is not true of a function a Sphinx build resolves and ubCode does not.
A call that ubCode does implement but that fails when it runs —
copy("status") on a need that has no status,
or any call needing a need when there is none —
renders ?? and is not reported.
A Sphinx build reports these: it renders the same ?? and logs, for example,
Error while executing function 'copy': Option status not found in need REQ_002.
ubCode is silent, and that is a known gap rather than a considered quietness:
the value is computed while the page is being rendered,
which is after the point where ubCode attaches findings to a source location.
Reporting it is planned.
Everything ubCode can decide from the text you wrote — an unparseable call,
an unknown function name, a bad argument list — is reported, once, at the place
you wrote it.
Dynamic function parsing is enabled by default and can be controlled globally or per-field/per-link:
[needs]
parse_dynamic_functions = false # disable globally (sphinx-needs ≥ 7.0)
[needs.fields.my_field]
parse_dynamic_functions = true # re-enable for this field (sphinx-needs ≥ 6.3)
[needs.links.implements]
parse_dynamic_functions = true # re-enable for this link (sphinx-needs ≥ 6.3)
- parse_dynamic_functions
Type:
boolean(default:true)Whether dynamic functions are parsed by default. Applies to extra fields and links unless overridden per-field/per-link. Core fields are not affected by this global setting.
The global
parse_dynamic_functionsoption was added in sphinx-needs 7.0 (needs_parse_dynamic_functions). Per-field and per-linkparse_dynamic_functionswas added in sphinx-needs 6.3.
filter_data¶
Type: object (default: {})
Additional context variables that can be referenced in filter expressions throughout your needs configuration. This allows you to define custom variables for conditional logic in multiple contexts.
Where filter_data can be used:
Predicate default filters — in
global_optionspredicates (orpredicateson individual fields/links in sphinx-needs ≥ 7.0) to conditionally set default valuesneedimport item filters — to filter which needs are imported from external sources
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.
json_include_link_conditions¶
Type: boolean (default: true)
Whether outgoing link values keep the [condition] suffix they were authored with.
With the default true, a conditional link is written in full —
"links": ["REQ_001[status == \"open\"]"] —
and a target authored twice with different conditions stays two entries.
With false, only the target is written — "links": ["REQ_001"].
Backlinks never carry a condition, so they are unaffected either way.
Note
One difference from Sphinx-Needs on the false path:
a target authored twice with different conditions
becomes a single bare entry in ubCode,
where Sphinx-Needs writes it twice (["REQ_001", "REQ_001"]).
ubCode removes a duplicate the bare form cannot distinguish;
Sphinx-Needs decides the question earlier, while the conditions are still visible.
The same narrowing means len() in a :filter: counts such a target once
in ubCode and twice in Sphinx-Needs, at any setting of this key.
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.