Dutch legislation follows a hierarchical delegation pattern: a formal law (wet) delegates authority to lower regulatory layers. For example, the Wet op de zorgtoeslag, article 4, delegates the determination of the standard premium (standaardpremie) to the minister via a ministerial regulation.
The engine previously supported this via a top-down resolve action: the higher law explicitly searches for matching lower regulations using legal_basis indexes and select_on criteria. This inverts the real legal relationship. In practice, a ministerial regulation opens with “In consideration of article 4 of the Wet op de zorgtoeslag” (“Gelet op artikel 4 van de Wet op de zorgtoeslag”). It registers itself as filling in a delegated term.
The top-down approach has limitations:
Implement Inversion of Control (IoC) via two new constructs in the schema:
open_terms (on article-level machine_readable)Declares abstract values that can or must be filled by implementing regulations:
Since schema v0.7.0 this is the only place an open norm is recorded, whether or not the article names who fills it. Where the law names a filler, delegated_to and delegation_type record the name and the required regulatory layer, and the engine rejects an implementation from another layer. Where the law names nobody (“redelijkerwijs”, “in bijzondere gevallen”, “voor zover dat naar het oordeel van het bestuursorgaan”), both fields are absent and any competent authority may fill the term through implementing policy. The resolver already reads an absent delegation_type as “no requirement on the layer”, so the wider reading costs it nothing.
The concept does not change with the question of whether the law appoints a filler. That question decides who may answer, and the term is open either way. Before v0.7.0 the schema had a second field, norm_gaps, whose kind distinguished delegated, policy and open: three ways the same open norm gets filled, recorded in a channel the engine does not read. RFC-031 has the measurement that ended that split.
The state of the corpus has no place here. Whether the filling regulation has been harvested is no property of the law, it is not visible in the file that would have to record it, and it stops being true the next time something is harvested. That question belongs to the resolve step and the work queue of RFC-026.
implements (on article-level machine_readable)Declares that an article fills an open term from a higher-level law:
implements declarations at law load timeopen_terms, the engine looks up implementationsvalid_from <= calculation_date). Candidates with no valid version for the requested date are excluded. This ensures that e.g. the 2025 standard premium (standaardpremie) is used for a 2025 calculation, even if a 2026 version is also loadedgemeente_code). A scoped regulation only matches when the execution parameters contain the same value. Unscoped (national) regulations always matchvalid_from wins). When candidates have the same layer and date, this is ambiguous: the engine returns an error rather than silently picking one. This is a law authoring error that needs fixingdefault: execute the default actions blockrequired: true + no default: errorrequired: false + no default: skip (traced)ResolutionContext.visited), a CircularReference error is raised. The engine treats circular dependencies as a law authoring problem and does not work around them. The cycle detection key uses \0 (null byte) as separator to prevent key collisions when law IDs or article numbers contain #regulatory_layer matches the open term’s delegation_type. If a municipal ordinance (gemeentelijke verordening) tries to implement a term delegated to a minister, the engine rejects it with a clear erroropen_terms and implements arrays are validated against MAX_ARRAY_SIZE at law load time, preventing resource exhaustionTemporal filtering is needed for correctness when multiple versions of an implementing regulation are loaded (e.g., the 2024 and 2025 standard premium (standaardpremie)). The mechanism works at two levels:
Same $id, multiple versions. The resolver stores multiple versions of the same law (keyed by $id), sorted newest-first. get_law_for_date filters these by valid_from <= calculation_date and returns the most recent valid version. So for a 2025-01-15 calculation with both regeling_standaardpremie versions loaded, the 2025 version is selected. A 2026 version with valid_from: 2026-01-01 would be excluded because 2026-01-01 <= 2025-01-15 is false.
Different $ids implementing the same term. If two separate law IDs both declare implements for the same open term, the implements_index contains entries for both. At resolution time, find_implementations calls get_law_for_date for each candidate independently, so a future-dated law is excluded before it ever reaches priority resolution.
Invariant: valid_from must be present on implementing regulations. The temporal filter uses is_none_or, meaning a regulation without valid_from passes the date check unconditionally, it matches every calculation date. This is by design for laws where the effective date is unknown, but for implementing regulations it undermines temporal correctness. Law authors must ensure that every regulation with an implements declaration has an explicit valid_from date. The schema does not enforce this (since valid_from is optional for other use cases), so this is a convention that must be maintained through review.
The implements_index is built at load time and is date-independent. It records all implementing relationships across all loaded versions. Temporal filtering happens at query time in find_implementations, which is the correct place because the calculation date is only known at execution time.
source.outputWhen multiple articles in the same law need an open term value, only one article should declare the open_terms and serve as the single point of delegation. Other articles reference it via source.output (see RFC-001, Section 9: Input Source Consolidation) without source.regulation:
This ensures the flow is: article 2 → article 4 → IoC → regulation (regeling), rather than article 2 bypassing article 4 and reaching into the regulation (regeling) directly.
Open terms can have an optional default block containing actions. This makes the article executable standalone while allowing refinement by lower regulations. The implementing regulation replaces the default entirely and must handle all cases.
This pattern is more common at lower regulatory layers (a policy rule with a reasonable default that can be overridden by implementation policy) but the mechanism works on all layers.
Defaults also serve a legal correctness role. For example, Participatiewet article 8’s open terms have default blocks with verlaging_percentage: 0 and duur_maanden: 0. Legal basis: art. 18 lid 2 says “verlaagt … overeenkomstig de verordening”, no ordinance (verordening) means no reduction (verlaging). A missing ordinance now results in full social assistance (bijstand) rather than a DelegationError.
source.delegation + select_on + legal_basis_for mechanism with a single, cleaner patternsource.delegationThe old delegation mechanism (source.delegation + select_on) forced the higher law to encode how to find its implementations:
This is backward. The Participatiewet doesn’t know which municipalities (gemeenten) have ordinances (verordeningen); it just delegates. The municipal ordinance (gemeentelijke verordening) knows which law (wet) it implements. IoC corrects this by letting the implementing regulation declare the relationship:
The engine resolves which municipality’s (gemeente) ordinance (verordening) applies; the law itself does not encode this. The engine already knows the execution scope (e.g., gemeente_code: GM0384) from its parameters. The find_implementations method uses a matches_scope helper that checks all scope fields on the candidate law against execution parameters. Currently supports gemeente_code; designed for easy extension to provincie_code etc. This eliminates select_on, legal_basis_for, and source.delegation entirely: all delegation flows through open_terms + implements.
When resolving open terms, the engine does not forward all execution parameters to implementing articles. It uses filter_parameters_for_article to only pass parameters declared in the implementing article’s execution.parameters section (principle of least privilege).
Alternative 1: Extend enables field
enables field was added to the schema in v0.3.1 but never implemented in the engineopen_terms is a cleaner separationenables sit on an article that already has a gap about the same delegation, so the field duplicated a record without adding a fact. From v0.3.1 to v0.7.0 it had no reader in the engineAlternative 2: implements as top-level metadata
implements at the law level, alongside legal_basisimplements belongs at the article levelAlternative 3: Default as separate construct
fallback or default_implementation conceptdefault directly on the open term, keeping the declaration and its fallback togetherpackages/engine/src/priority.rs for lex superior/lex posterior resolutionimplements_index in RuleResolver keyed by (law_id, article, open_term_id)evaluate_article_with_service() before pre_resolve_actions()PathNodeType::OpenTermResolution, ResolveType::OpenTerm| Pattern | Use when |
|---|---|
IoC (open_terms + implements) | Any delegation: a higher law delegates a value to a lower regulation (with or without scope) |
Same-law reference (source.output) | Internal: one article needs a value produced by another article in the same law (see RFC-001 §9) |
External reference (source.regulation) | Direct reference: one law needs a specific value from another law (see RFC-001 §9) |
The old source.delegation + select_on + legal_basis_for pattern is superseded by IoC and will be phased out.
The table has no fourth pattern. An open norm with no filler named in the law is IoC too, with delegated_to and delegation_type left empty, so it is the same mechanism. The language expresses the term fine and the content sits elsewhere, so no engine operation resolves it and it is never a marking.
source.delegation to open_termssource.delegation, select_on, and legal_basis_for from the schemaThis RFC replaces the original RFC-003 (Delegation Pattern), which described a top-down delegation model using source.delegation + select_on + legal_basis_for. The IoC model described here inverts that relationship.
Schema v0.7.0 widened open_terms to cover every open norm, including the one no regulation is appointed to fill, and removed enables and norm_gaps. That is a change to the reach of the construct decided here, and RFC-031 records why.
schema/v0.4.0/schema.jsonschema/v0.7.0/schema.json, for the widened open_termscorpus/regulation/nl/wet/wet_op_de_zorgtoeslag/2025-01-01.yaml and corpus/regulation/nl/ministeriele_regeling/regeling_standaardpremie/2025-01-01.yamlcorpus/regulation/nl/gemeentelijke_verordening/amsterdam/apv_erfgrens/2024-01-01.yaml and corpus/regulation/nl/gemeentelijke_verordening/diemen/afstemmingsverordening_participatiewet/2015-01-01.yamlAn exploration by Bureau Architectuur of the Dutch Ministry of Economic Affairs and Climate Policy into the possibilities of transparent, executable legislation.
GitHub repository
How it works
Stay informed
Roadmap (Dutch)
Documentation
Research
Bureau Architectuur
Ministry of Economic Affairs and Climate Policy