Skip to content

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.

A field’s value is a one-key object whose key names the type:

Policy typeJSON
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.

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.

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.

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_failures like a constraint that was simply false, and only that entry’s reason tells the two apart.

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 typeIn 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]
Objectnot 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.

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 typeAPI (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 fields wrapper.
  • 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.

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.