Build
The JavaScript SDK lets you parse, identify, and process Blue documents from your own code. You are not calling isolated features — you are loading documents, computing BlueIds, building timeline entries, and processing them through the official Blue document processor.
An important mental shift: you are working with Blue documents and interactions, not just "resources" in the generic SaaS sense. Common SDK actions:
prevEntry.blueIdnpm install @blue-labs/language @blue-labs/document-processor @blue-repository/types
The processor pulls in a QuickJS WebAssembly runtime so it can execute deterministic JavaScript inside workflow steps. That makes browser bundling non-trivial; the SDK is most ergonomic when run in Node.js (a serverless function, an API route, a worker, a CLI).
import { Blue, BlueIdCalculator } from "@blue-labs/language";
import { createDefaultMergingProcessor } from "@blue-labs/document-processor";
import { repository } from "@blue-repository/types";
const blue = new Blue({
repositories: [repository],
mergingProcessor: createDefaultMergingProcessor(),
});
const node = blue.yamlToNode(`
name: Demo Counter
counter: 0
`);
const blueId = await BlueIdCalculator.calculateBlueId(node);
// e.g. "67WudUZqbHHoVkDhWiX1r7NQtP8igMCan2kQ2qDSGDDZ"import { Blue } from "@blue-labs/language";
import {
DocumentProcessor,
ContractProcessorRegistryBuilder,
createDefaultMergingProcessor,
} from "@blue-labs/document-processor";
import { repository } from "@blue-repository/types";
const blue = new Blue({
repositories: [repository],
mergingProcessor: createDefaultMergingProcessor(),
});
const registry = ContractProcessorRegistryBuilder.create()
.registerDefaults()
.build();
const processor = new DocumentProcessor({ blue, registry });
const document = blue.yamlToNode(`
name: Demo Counter
counter: 0
contracts:
ownerChannel:
type: Conversation/Timeline Channel
timelineId: owner-1
increment:
type: Conversation/Operation
channel: ownerChannel
request:
type: Integer
incrementHandler:
type: Conversation/Sequential Workflow Operation
operation: increment
steps:
- name: ApplyIncrement
type: Conversation/Update Document
changeset:
- op: replace
path: /counter
val: ${event.message.request + document('/counter')}
`);
const init = await processor.initializeDocument(document);
// init.document, init.totalGas, init.triggeredEvents, init.capabilityFailure
const event = blue.jsonValueToNode({
type: "Conversation/Timeline Entry",
timeline: { timelineId: "owner-1" },
message: {
type: "Conversation/Operation Request",
operation: "increment",
request: 5,
allowNewerVersion: true,
},
});
const result = await processor.processDocument(init.document, event);
// result.document → the next document state
// result.totalGas → gas consumed by this processing pass
// result.triggeredEvents → events emitted during processing (often empty)
// result.capabilityFailure / result.failureReason → set when a contract is unsupportedresult.document is the new BlueNode. Use blue.nodeToJson(...) to serialize it.result.totalGas is a real number — what the processor actually consumed. The document stepper shows it directly. See Gas and JS processing.capabilityFailure: true and a failureReason. The processor does not pretend it can handle what it cannot.If you are stepping a document yourself, set prevEntry.blueId on each new entry to the BlueId of the previous entry on the same timeline:
const previousEntryNode = blue.jsonValueToNode(previousEntryJson);
const previousBlueId = await BlueIdCalculator.calculateBlueId(previousEntryNode);
const nextEntry = {
type: "Conversation/Timeline Entry",
timeline: { timelineId: "owner-1" },
prevEntry: { blueId: previousBlueId },
message: { /* ... */ },
};The document stepper uses these exact packages. Open it, load Counter, and you are watching the same code paths run on the same processor.