Value encoding
This page exists because the wire form is not described by the schema. In the
Orchestrator API spec, an intent’s attrs is an ObjectValue, and
ObjectValue.fields is declared additionalProperties: {} — an untyped map. The
only description of what belongs in it is a single example, added to
AuthoredIntent in v0.10.0. Everything below is read from
policy_engine::types, which is what actually parses these values.
Values are tagged by type
Section titled “Values are tagged by type”A field’s value is a one-key object whose key names the type:
| Policy type | JSON |
|---|---|
bool | {"Bool": true} |
int | {"Int": 3000} |
string | {"String": "new"} |
date | {"Date": {"year": 2026, "month": 6, "day": 12}} |
Set<int> | {"Set": {"Int": [1, 2, 3]}} |
Set<string> | {"Set": {"String": ["Fantasy", "SciFi"]}} |
Set<date> | {"Set": {"Date": [{"year": 2026, "month": 6, "day": 12}]}} |
List<int> | {"List": {"element_type": "Int", "values": [{"Int": 1}, {"Int": 2}]}} |
A set is tagged twice: once as Set, then again by its element type. That is the
shape most easily got wrong.
A list is not a set with a different key. element_type names the element type
and every element carries its own tag, because Value::List.values is a
Vec<Value>. So a set’s elements are bare and a list’s are tagged:
{ "Set": { "Int": [1, 2, 3] } }{ "List": { "element_type": "Int", "values": [{"Int": 1}, {"Int": 2}, {"Int": 3}] } }{"List": {"element_type": "Int", "values": [1, 2, 3]}} is rejected. An empty
list still carries its element_type, with values an empty array.
A complete attrs object
Section titled “A complete attrs object”This is the example the v0.10.0 spec ships, for a policy declaring
acceptable_genres: Set<string>, condition: string and max_price_cents: int:
{ "fields": { "acceptable_genres": { "Set": { "String": ["Fantasy", "SciFi"] } }, "condition": { "String": "new" }, "max_price_cents": { "Int": 3000 } }}Note the fields wrapper: attrs is an ObjectValue, not a bare map.
Optional parameters are absent, not null
Section titled “Optional parameters are absent, not null”A parameter declared optional is omitted when it has no value — an absent key,
never null and never an empty set. An optional: constraint reading it is then
skipped. This matches the guidance for attrs itself in the
Orchestrator API: omit the field rather than sending
"attrs": null.
Where a wrong type surfaces
Section titled “Where a wrong type surfaces”Not in the same place for both sides, which matters when you are debugging. The rule is §9.1 Input Handling of the language reference; for a caller it means:
- Parameters are checked up front, against the policy’s declared schema. A wrong type fails immediately, before evaluation.
- Evidence fields and object attributes are checked when a constraint reads
them. A wrong type surfaces as a runtime error mid-evaluation: it arrives in
constraint_failureslike a constraint that was simply false, and only that entry’sreasontells the two apart.
Returned evidence is not tagged
Section titled “Returned evidence is not tagged”Nothing tagged travels the other way. The Orchestrator converts evidence to
plain JSON before returning it, so the evidence object in a status response or
a success/failure event carries raw values — which is what the API spec
declares it to be, an untyped object:
| Policy type | In attrs (tagged) | In a returned evidence object |
|---|---|---|
bool | {"Bool": true} | true |
int | {"Int": 3000} | 3000 |
string | {"String": "new"} | "new" |
date | {"Date": {"year": 2026, "month": 6, "day": 12}} | "2026-06-12" |
Set<int> | {"Set": {"Int": [1, 2, 3]}} | [1, 2, 3] |
List<int> | {"List": {"element_type": "Int", "values": [{"Int": 1}]}} | [1] |
Object | not a parameter type | { ... } |
There is no fields wrapper either: a returned evidence object is a bare field
map, and so is a nested object inside it.
Object has no attrs form because a parameter cannot be one. The compiler
rejects a parameter declared as bare Object or as List<Object>, so an object
reaches a policy only as evidence — which is the one direction this table
describes for it.
The conversion is one-way and lossy — a set and a list are both arrays, a date
is a string, an empty list keeps no element_type — so a returned evidence
object cannot be pasted back into attrs. Nothing asks you to: parameter values
come from whoever authored the intent, and evidence comes from the extractor,
which supplies plain JSON scalars of its own.
Not the CLI’s encoding
Section titled “Not the CLI’s encoding”policy-engine’s command line tags its values too, but not the same way, so a
CLI example still needs converting before it can go in attrs:
| Policy type | API (attrs) | CLI (--parameters, --evidence) |
|---|---|---|
int | {"Int": 3000} | {"int": 3000} |
string | {"String": "new"} | {"string": "new"} |
date | {"Date": {"year": 2026, "month": 6, "day": 12}} | {"date": "2026-06-12"} |
Set<string> | {"Set": {"String": ["Fantasy", "SciFi"]}} | {"Set<string>": ["Fantasy", "SciFi"]} |
List<int> | {"List": {"element_type": "Int", "values": [{"Int": 1}, {"Int": 2}, {"Int": 3}]}} | {"List<int>": [1, 2, 3]} |
Five differences:
- Scalar tags are lowercase.
- A collection names its element type inside the key, instead of nesting a second tag.
- A date is an ISO string, not a three-field object.
- The CLI’s top level is a bare field map, with no
fieldswrapper. - A CLI list holds bare elements where the API tags each one.
That last one is the trap when porting a list: {"List<int>": [1, 2, 3]} does
not become values: [1, 2, 3]. Each element has to be wrapped —
values: [{"Int": 1}, {"Int": 2}, {"Int": 3}] — or the API rejects the intent.
A CLI List<Object> is not a case of this. Its elements are bare field maps
whose tagged form is {"Object": {"fields": { ... }}}, but that conversion only
ever produces evidence: --evidence accepts an object, attrs cannot declare
one, so there is no valid intent to port such a value into.
Source
Section titled “Source”policy_engine::types::Value and SetValue (the Bool/Int/String/Date/
Set/List/Object variants), DateLit for the date shape, and §4 of the
compiler’s API specification for the absent-key and type-mismatch behaviour. The
tagged JSON is serde’s external tagging of those enums. The CLI form is §2.3 of
the compiler’s CLI documentation. The plain form of a returned evidence object is
the Orchestrator’s own conversion, and the evidence field descriptions in the
Orchestrator API spec.