Generate a Node.js SDK, call BAML functions, and read streamed results.
Call BAML from a Node.js application using a generated baml_sdk and the @boundaryml/baml-bridge runtime. These examples use Canary and the generator’s default preserve-case naming convention.
Generate the SDK
Start with a BAML project, then run these commands from its root:
The runtime uses ES modules. These commands initialize a new Node.js package; in an existing application, use its ES-module configuration.
Run generate add once. It adds a generator to baml.toml; baml generate writes baml_sdk/ beside that file. Regenerate after changing BAML source.
Match the runtime package version to the toolchain version printed by baml --version. If npm’s default release differs, install that exact version with npm install @boundaryml/baml-bridge@<version>.
Call a function
With return_number from Get started, create app.ts beside baml.toml:
Run it with npx tsx app.ts. It prints 42.
return_number runs synchronously and returns the value directly. Import from baml_sdk/index.js so the SDK initializes its runtime and type mappings.
Call asynchronously
The Node.js generator also exports an _async variant of each function. It returns a promise:
Replace app.ts with this version and run npx tsx app.ts again. It also prints 42.
Use the async variant when other JavaScript work needs to continue during the call, such as when a Node.js server waits for a model response. The synchronous variant blocks the JavaScript thread until the call finishes.
Compatibility
| Concern | TypeScript behavior |
|---|---|
| Calls | Node.js exports synchronous functions and promise-returning _async variants. |
| Names | The default preserve-case convention keeps BAML names. Async variants add _async. |
| Regeneration | Generated files are replaced; application edits belong elsewhere. |
Type mappings
| BAML | TypeScript | Notes |
|---|---|---|
string | string | Text values map directly. |
int / float | number | Keep integer constraints in mind at runtime boundaries. |
T[] | T[] | Element types remain explicit. |
T? | T | null | Do not use truthiness when zero or empty text is valid. |
class | Generated class | Fields preserve their declared names and types. |
enum | Generated enum | Import the generated symbol instead of duplicating literals. |
Call a model
Add the BAML source from Classify support tickets, run baml generate, and set OPENAI_API_KEY in the server’s environment:
Streaming
For an LLM function, the generator also exports a $stream_async function. Read partial values with nextAsync() until it returns ai.stream.Done, then call finalAsync() for the complete result:
Canary’s generated stream type currently omits the Done sentinel from nextAsync()’s return type. The example narrows from unknown to check the runtime sentinel.
A partial value can be null; it does not mean the stream has ended. Calls and stream reads can fail, so handle rejections at your application boundary.
Transition rules
- Await the _async variants before using their results. Synchronous variants block until completion.
- Regenerate after changing a BAML function, class, enum, or generator configuration.
- Treat partial streaming values separately from the final typed result.
Gotchas
- Never edit generated client files by hand.
- Keep secret-bearing client construction on the server side of web applications.
- Do not use truthiness to test nullable numbers or strings when zero or empty text is valid.
- Import generated enum symbols instead of duplicating their literal values.