Core model·Documents and types

Documents and types

A Blue document is structured content selected from a graph. It may be as small as a single scalar value or as large as a business process with embedded child documents and contracts. The same language rules apply in both cases: nodes have payloads, metadata, type references, schema constraints, and identity.

This page explains the content model before runtime processing. Timelines, operations, workflows, and BEX matter later, but they all sit on top of the same document and type system.

§Nodes are the unit of meaning

A Blue document is a rooted slice of a Blue graph. The root can be an object, list, scalar, or pure reference. Most authored examples use YAML objects because they are easier to read:

YAML
1name: Counter
2counter: 0

This document has an object root with two ordinary fields. After preprocessing, counter: 0 is understood as an integer scalar value in the Blue node model.

A node has one payload kind:

Payload kindExampleNotes
Scalarcounter: 0Text, integer, double, and boolean scalar values are inferred during baseline preprocessing when no explicit type is provided.
Object fieldsprice: { amountMinor: 49900, currency: PLN }Most business documents are object nodes.
List itemsitems: [hotel, restaurant]List positions are content. Empty positions are not silently deleted.
Pure referenceblueId: <BlueId>The node is exactly a reference to content addressed by BlueId.

Nodes can also carry Blue language metadata. Common metadata fields include name, description, type, schema, itemType, keyType, valueType, mergePolicy, contracts, and blueId.

That last sentence is easy to skim past, but it is important. In Blue, metadata is still content. A canonical type node's description, for example, can be identity-bearing when it defines the type's semantics. Do not treat canonical type descriptions like website copy unless the registry intends them to be editorial text outside the identity-bearing node.

§A type is also a document

In many systems, schemas and data live in different worlds. Blue does not make that split. A type is a Blue node, and a document can point to it.

Here is a reusable money amount type:

YAML
1name: Money Amount
2description: Amount in minor currency units.
3amountMinor:
4 type: Integer
5 schema:
6 minimum: 0
7currency:
8 type: Text
9 schema:
10 minLength: 3
11 maxLength: 3

Here is a document that uses it:

YAML
1name: Package Price
2type:
3 blueId: <MoneyAmountBlueId>
4amountMinor: 49900
5currency: PLN

The type field says: this node must conform to the referenced type. During resolution, a processor uses a node provider to load the referenced content, merge the inherited type information with the instance, and validate the result.

The instance does not need to repeat every inherited detail. It only supplies the values that make this specific node different from the type definition.

§Type names are authoring conveniences

You will often see examples like this:

YAML
1type: Integer

That is not the canonical identity-bearing form. It is source authoring sugar. During preprocessing, core type aliases such as Integer, Text, Double, Boolean, Dictionary, and List are replaced by canonical type references from the Blue type registry.

When you define your own aliases, declare them explicitly through the root blue directive:

YAML
1blue:
2 imports:
3 MoneyAmount:
4 blueId: <MoneyAmountBlueId>
5
6name: Package Price
7type: MoneyAmount
8amountMinor: 49900
9currency: PLN

After preprocessing, MoneyAmount in a type position becomes:

YAML
1type:
2 blueId: <MoneyAmountBlueId>

This keeps source pleasant without making portable identity depend on hidden local names.

§Pure references are not typed objects

A pure BlueId reference is the entire node:

YAML
1blueId: <MoneyAmountBlueId>

Do not add sibling fields to a pure reference:

YAML
1# Invalid as a pure reference shape
2blueId: <MoneyAmountBlueId>
3amountMinor: 49900
4currency: PLN

If you want a value that is typed by a referenced type, put the reference under type:

YAML
1type:
2 blueId: <MoneyAmountBlueId>
3amountMinor: 49900
4currency: PLN

The distinction avoids an ambiguity that has caused problems in many data systems: is this object the referenced thing, or is it a new local thing with a related type? Blue makes the answer structural.

§Extension is additive

Blue's type system is built around compatibility. A more specific type can add fields and compatible constraints, but it cannot contradict the parent.

Start with a general product:

YAML
1name: Product
2sku:
3 type: Text
4 schema:
5 required: true
6 minLength: 3
7price:
8 amountMinor:
9 type: Integer
10 schema:
11 minimum: 0
12 currency:
13 type: Text
14 schema:
15 minLength: 3
16 maxLength: 3

Now specialize it into an offer:

YAML
1name: Offer
2type: Product
3kind: Offer
4status:
5 type: Text
6 schema:
7 enum: [draft, active, retired]
8seller:
9 name:
10 type: Text

Then specialize further into a package offer:

YAML
1name: Weekend Package Offer
2type: Offer
3offerKind: package
4included:
5 hotel:
6 title:
7 type: Text
8 restaurant:
9 title:
10 type: Text

Every Weekend Package Offer is also an Offer. Every Offer is also a Product. That statement should remain true after resolution. Blue enforces it by rejecting incompatible overlays.

Fixed values cannot be silently replaced

If Offer fixes kind: Offer, a child cannot change it to kind: Voucher:

YAML
1# Invalid: contradicts inherited fixed value
2name: Broken Package Offer
3type: Offer
4kind: Voucher

Fixed values are useful because they give type hierarchies discriminators without relying on external enum mapping code. But fixed values only work if descendants cannot quietly override them.

Field types must remain compatible

If Product.price.amountMinor is an integer, a child cannot turn it into text:

YAML
1# Invalid: inherited field type is Integer
2name: Broken Product
3type: Product
4price:
5 amountMinor:
6 type: Text

A child may refine a field with a compatible subtype or additional constraints. It may not weaken the field so existing Product consumers would be surprised.

Schema constraints accumulate

Schema constraints are attached through schema:

YAML
1name: SKU
2value:
3 type: Text
4 schema:
5 minLength: 3
6 maxLength: 32

A more specific type can add a stricter compatible constraint:

YAML
1name: Merchant SKU
2type: SKU
3value:
4 schema:
5 minLength: 8

The effective value must satisfy both constraints. If a child adds an incompatible constraint, resolution fails. Blue does not let the child pretend the inherited rules disappeared.

Useful schema keywords include required, minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItems, uniqueItems, minFields, maxFields, and enum.

§Required means semantically present

Fields are optional by default. To require a field, attach schema.required: true to the child field declaration:

YAML
1name: Merchant Order
2kind: Order
3orderKind:
4 type: Text
5 schema:
6 required: true
7confirmation:
8 status:
9 type: Text
10 schema:
11 required: true
12 enum: [pending, confirmed, rejected]

A metadata-only declaration does not satisfy the requirement. The resolved descendant must supply meaningful content for the field. This matters for workflow documents, where a field may be declared in the type but not filled until an operation is delivered.

§Resolution creates the runtime view

Source is what you author. Resolved view is what processors use to understand inherited fields, fixed values, schema constraints, and referenced types.

Given this type:

YAML
1name: Confirmation State
2status:
3 type: Text
4 schema:
5 enum: [pending, confirmed, rejected]

And this instance:

YAML
1blue:
2 imports:
3 ConfirmationState:
4 blueId: <ConfirmationStateBlueId>
5
6type: ConfirmationState
7status: confirmed

The resolver loads <ConfirmationStateBlueId>, expands the type information, validates that confirmed is one of the allowed values, and produces a resolved view where both the inherited constraints and instance value are available.

The canonical identity input may be more compact than the resolved view. If a field is fully inherited and does not need to be repeated for identity, canonicalization can omit it. That is why raw source comparison is not the right way to compare Blue documents.

§Lists preserve position

Blue lists are not just arrays that can be cleaned casually. Position is content. If a source list contains null or an empty object as an element, preprocessing normalizes that position to a placeholder rather than deleting it:

YAML
1items:
2 - hotel
3 - null
4 - restaurant

Normalizes to a list with an explicit empty position:

YAML
1items:
2 - hotel
3 - $empty: true
4 - restaurant

That placeholder affects identity. It is not the same as removing the item and shifting later indices.

List overlays also have controls for inherited lists. A $previous anchor can state which inherited prefix is being extended, and $pos can refine a specific inherited position when the list's merge policy allows positional overlays:

YAML
1items:
2 - $previous:
3 blueId: <InheritedItemsBlueId>
4 - $pos: 1
5 value: upgraded dinner
6 - value: late checkout

Use these controls only when you are intentionally working with inherited list content. Ordinary business documents usually do not need them at first. They become important when a type or template contributes list content and a descendant wants to refine it without restating the entire list.

§Contracts are content too

You will see a contracts map in processable documents:

YAML
1contracts:
2 ownerChannel:
3 type: Coordination/Timeline Channel
4 timelineId: counter-demo
5
6 increment:
7 type: Coordination/Operation
8 channel: ownerChannel

From the language perspective, contracts is a reserved field that affects document content and identity like any other field. The runtime meaning is defined by the Contracts and Processor specification and by packages such as Coordination.

This separation is useful. The Blue Language can define how contracts participates in content, resolution, and identity without hard-coding every possible contract runtime into the language.

§Embedded documents are ordinary child content with scope

In the weekend package example, the package order embeds a PayNote and merchant order snapshots:

YAML
1embeddedDocs:
2 packagePayNote:
3 type: PayNote/PayNote
4 kind: PayNote
5 orders:
6 hotelOrder:
7 kind: Order
8 orderKind: hotel
9 restaurantOrder:
10 kind: Order
11 orderKind: restaurant

Language-wise, these are child nodes. Runtime-wise, contracts such as Core/Embedded Node Channel and Core/Process Embedded let events from those child scopes influence parent or sibling workflows.

Keep those two views separate. Embedding is not a magical database join. It is content in the parent document, with runtime contracts deciding how child processing is observed.

§The practical authoring model

When authoring Blue documents, follow these rules:

  1. Write the state first. Make the current business facts readable before adding contracts.
  2. Use type to make structure explicit. Use pure blueId only when the node is exactly a reference.
  3. Prefer schema constraints for deterministic validation over prose instructions.
  4. Treat fixed inherited values as promises. Do not override them.
  5. Treat lists carefully. Empty list positions and inherited list controls affect identity.
  6. Keep host-only data outside the Blue document unless it is intended to be identity-bearing content.
  7. Put explanatory tutorial text in docs, not inside canonical type nodes, unless that text intentionally defines the type's semantics.

The rest of the runtime builds on these rules. A workflow can only be deterministic if the document it processes has deterministic structure and identity.