Skip to main content

Embedded Documents

Blue documents can embed other documents that have their own contracts and behaviors. This creates compositional structures where both parent and embedded documents process events independently while maintaining a consistent state. This page explains how to use these embedded documents and ensure they're properly processed.

Moving Beyond Simple Documents

While previous examples focused on single documents with contracts, real-world applications often require complex document structures with embedded components:

  • Payment agreements with embedded payment method documents
  • Event contracts with embedded ticket verification systems
  • Legal agreements with embedded identity verification flows

These embedded documents can have their own channels, workflows, and processing states. However, Blue doesn't automatically process every embedded document—it requires explicit instructions about which embedded documents should be active.

Extending Our Online Lesson Example

Let's extend our online tutoring platform to include payment processing. In addition to setting the price as before, we'll now have the teacher provide a complete payment document:

name: Online Lesson with Payment
type: Online Lesson
payment:
description: Payment details for this lesson
type: Stripe Payment
contracts:
# Enable processing of the embedded payment document
embedded:
type: Process Embedded
paths:
- /payment

# Extend the existing workflow to set the payment
confirmLessonWorkflow:
steps:
- blueId: bT4vHkzPa8Ypd5KJsLNcA3FV6xDqbnG1pcQx2tq16z5 # BlueId of the original steps
- name: UpdatePayment
type: Update Document
changeset:
- op: replace
path: /payment
val: ${event.payment}

For now, we're simply embedding the payment document so it's included in processing. In the Events section, we'll explore how this payment document can interact with the parent document through events.

For details on how Stripe Payments might work with Blue, see Stripe Payment Integration.

Workflow Extension Pattern

In the example above, we're not redefining the entire workflow - we're extending it. By referencing the BlueId of the existing steps defined in the "Online Lesson" type and then adding another step, we're effectively saying "do everything the original workflow does, then do this additional step."

This builds on Blue's type extending mechanism and creates a clean inheritance pattern where we maintain all the original functionality while adding payment-specific behavior.

Explicit Processing with Process Embedded

The key to working with embedded documents is the Process Embedded contract:

embedded:
type: Process Embedded
paths:
- /payment
- /gameStats

This contract tells the processor exactly which embedded documents should be processed. Without this declaration, embedded documents remain static data—their contracts aren't evaluated, and they don't receive events.

The Rationale for Selective Processing

Consider a document with multiple embedded references:

name: Order #12345
customer:
blueId: HVkRPb4L3EQey6rdwypi7na7ATz5P3PLH8Q3PSy2WQ94 # Customer profile
payment:
blueId: 2qCKS6yS11cYGabM3nPXsbd6DYK4m6WNrEqGGyySDCaD # Payment document
productCatalog:
blueId: kL9mN0pQrStUvWxYz1A2B3C4D5E6F7G8H4FGh7j8 # Entire catalog with thousands of products

Automatically processing every BlueId reference would be inefficient, especially for large references like product catalogs. The explicit Process Embedded approach ensures processors only extend and process the specific paths needed for the document's functionality.

How Embedded Document Processing Works

When a processor encounters a Process Embedded contract, it:

  1. Extends all specified paths to retrieve the full embedded documents
  2. Evaluates all contracts within those embedded documents
  3. Processes events for both the parent and embedded documents
  4. Maintains checkpoints independently for each document
  5. Returns the updated parent document with updated embedded documents

This creates a composite processing environment where multiple documents work together while maintaining their independence.

Temporal Synchronization with Checkpoints

It may happen that an embedded document is temporally behind the parent document (has an earlier checkpoint). When this occurs, the processor must handle the temporal differences.

Channel Coordination During Embedding

When multiple documents with their own contracts are processed together, they must share a common coordinator to determine event ordering. This coordinator ensures consistent event processing across all embedded documents.

For documents with different temporal positions, the coordinator will process earlier events first. This means:

  1. The coordinator collects events from all channels across all processed documents
  2. It orders these events according to its coordination strategy
  3. When an embedded document is behind temporally, its events will naturally come first in the ordered sequence
  4. The embedded document will process all its backlogged events before the parent document processes newer events

This isn't the processor "recognizing a gap"—it's simply the coordinator doing its normal job of chronologically ordering events from all available channels.

Embedded Node Channels

Embedded documents can also serve as event sources through the Embedded Node Channel contract:

gameEventsChannel:
type: Embedded Node Channel
path: /game

This contract treats events from the embedded document as if they were coming from a separate channel, allowing the parent document to listen for and react to them.

Example: Historical Game Processing

This example shows how to embed a historical game document and track specific events:

name: LeBron Blocks Counter
blocks: 0
game:
blueId: 7UEBwTmRMfQ92rGt4vHkzPa8Ypd5KJsLNcA3FV6xDqbn # Cavs-Warriors Game 7 Finals 2016
contracts:
# Enable processing of the embedded game
embedded:
type: Process Embedded
paths:
- /game

# Channel for game events
gameChannel:
type: Embedded Node Channel
path: /game

# Listen for blocking events
blocksTracker:
type: Sequential Workflow
channel: gameChannel
event:
type: Blocked Shot
player: LeBron James
steps:
- type: Update Document
changeset:
- op: replace
path: /blocks
val: ${document('/blocks') + 1}

How This Works

In this example, we're embedding a game from 2016 into a current document. When processing begins:

  1. The coordinator will see that the game document is temporally far behind (from 2016)
  2. All game events will be sequenced before any current-day events
  3. The game events will be processed in chronological order, incrementing the blocks counter
  4. This effectively "replays" the historical game within our document

We're essentially playing back a complete game recording within our document, with our contracts reacting to it. Since the game is from the past, all its events will be processed before any current events from other channels.

Circular References

To learn how to address circular references go to Advanced Topics - Circular References