npm package: @flowlines/sdk
Description: Capture LLM traces from your Node.js application and send them to [Flowlines](https://flowlines.ai).
Latest version: 0.1.6

# @flowlines/sdk

Capture LLM traces from your Node.js application and send them to [Flowlines](https://flowlines.ai).

The SDK automatically instruments AI libraries (OpenAI, Anthropic, Cohere, Bedrock, LangChain, and more) and exports only LLM-relevant spans to the Flowlines backend.

## Installation

```bash
npm install @flowlines/sdk
```

## AI Coding Agent Integration

If you use an AI coding agent, you can install the Flowlines skill so your agent knows how to integrate the SDK into your project:

```bash
npx skills add flowlines-ai/skills
```

Then, just ask your agent to integrate Flowlines into your project.

## Quick Start

### Step 1: Initialize the SDK

Create a separate instrumentation file and load it before your application:

```ts
// instrumentation.ts
import { Flowlines } from "@flowlines/sdk";

Flowlines.init({
  apiKey: "your_flowlines_api_key",
});
```

Start your application with:

```bash
node --import ./instrumentation.ts app.ts
```

> Using Next.js, an existing OpenTelemetry setup, or need more control over import order? See [Integration Guides](#integration-guides).

### Step 2: Attach context to your LLM calls

Use `Flowlines.context()` to attach a user ID and session ID to all LLM spans created within a callback:

```ts
import { Flowlines } from "@flowlines/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

await Flowlines.context({ userId: "user_123", sessionId: "sess_456" }, async () => {
  const response = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [{ role: "user", content: "Hello!" }],
  });
});
```

### Step 3: Fetch and use memory

Use `Flowlines.getMemory()` to retrieve memory context from the Flowlines backend for a given user. The returned string should be injected into your LLM conversation — for example, as a system message:

```ts
import { Flowlines } from "@flowlines/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

const memory = await Flowlines.getMemory({
  userId: "user_123",
  sessionId: "sess_456",
});

await Flowlines.context({ userId: "user_123", sessionId: "sess_456" }, async () => {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    {
      role: "system",
      content: `You are a helpful assistant.\n\n${memory}`,
    },
    { role: "user", content: "What were we talking about last time?" },
  ];

  const response = await openai.chat.completions.create({
    model: "gpt-4",
    messages,
  });
});
```

`getMemory()` returns the `memory` field from the response serialized as a JSON string, or an empty string if no memory is found.

### Step 4: End a session

Use `Flowlines.endSession()` to mark a session as ended on the Flowlines backend. This flushes all pending spans before sending the end-session request, ensuring the backend has received all traces for the session.

```ts
await Flowlines.endSession({
  userId: "user_123",
  sessionId: "sess_456",
});
```

## Core Concepts

### Attaching context to spans

`Flowlines.context()` accepts a user ID and session ID (both required), plus an optional agent ID. All LLM spans created within the callback will carry these attributes:

```ts
await Flowlines.context({ userId: "user_123", sessionId: "sess_456", agentId: "agent_1" }, async () => {
  // Spans will carry user_id, session_id, and agent_id
});
```

Calls can be nested. Inner values override outer ones on conflict.

### Fetching memory

`Flowlines.getMemory()` retrieves memory context for a given user:

```ts
const memory = await Flowlines.getMemory({
  userId: "user_123",
  sessionId: "sess_456",  // optional
  agentId: "agent_1",     // optional
  view: "summary",        // optional
});
```

Returns the `memory` field from the response serialized as a JSON string, or an empty string if no memory is found.

### Ending a session

`Flowlines.endSession()` marks a session as ended. It flushes all pending spans before sending the request so the backend has the complete trace data:

```ts
await Flowlines.endSession({
  userId: "user_123",
  sessionId: "sess_456",
});
```

### Graceful shutdown

Call `shutdown()` before your process exits to flush pending spans:

```ts
process.on("SIGTERM", async () => {
  await Flowlines.shutdown();
  process.exit(0);
});
```

## Integration Guides

### Standalone (default)

If your application **does not already use OpenTelemetry**, this is the simplest way to get started. The SDK creates and manages an OpenTelemetry `NodeSDK` instance internally.

The SDK instruments AI libraries by monkey-patching their module exports. This only works if `Flowlines.init()` runs **before** those libraries are imported. In ESM applications, all `import` statements are hoisted and executed before any other code — so putting `Flowlines.init()` at the top of your file is not enough.

**Option A: Use `--import` (ESM) or `--require` (CJS)**

Create a separate instrumentation file and load it before your application:

```ts
// instrumentation.ts
import { Flowlines } from "@flowlines/sdk";

Flowlines.init({
  apiKey: "your_flowlines_api_key",
});
```

Then start your application with:

```bash
# ESM
node --import ./instrumentation.ts app.ts

# CJS
node --require ./instrumentation.js app.js
```

**Option B: Use `instrumentModules`**

Pass imported modules explicitly — this works regardless of import order:

```ts
import OpenAI from "openai";
import { Flowlines } from "@flowlines/sdk";

Flowlines.init({
  apiKey: "your_flowlines_api_key",
  instrumentModules: { openAI: OpenAI },
});
```

See [Selecting which libraries to instrument](#selecting-which-libraries-to-instrument) for the full list of supported keys.

### Next.js

Next.js supports an [instrumentation file](https://nextjs.org/docs/app/guides/instrumentation) that runs before the application starts, making it the ideal place to initialize the SDK.

#### 1. Create the instrumentation file

Export a `register` function — Next.js will call it once when a new server instance is initiated:

```ts
// instrumentation.ts (project root)
import { Flowlines } from "@flowlines/sdk";

export function register() {
  Flowlines.init({
    apiKey: process.env.FLOWLINES_API_KEY!,
  });
}
```

#### 2. Use `Flowlines.context()` in server-side code

The SDK only runs on the server. Use it in Route Handlers, Server Actions, or any server-side code:

```ts
// app/api/chat/route.ts
import { Flowlines } from "@flowlines/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

export async function POST(req: Request) {
  const { message, userId, sessionId } = await req.json();

  return Flowlines.context({ userId, sessionId }, async () => {
    const response = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [{ role: "user", content: message }],
    });
    return Response.json({ reply: response.choices[0].message.content });
  });
}
```

#### 3. Configure bundler externals

Next.js bundles server code with webpack or Turbopack, which can interfere with the SDK's automatic monkey-patching. To prevent this, mark `@flowlines/sdk` and any instrumented AI libraries as webpack externals in your `next.config.mjs`:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    instrumentationHook: true,
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      config.externals.push("@flowlines/sdk", "openai");
    }
    return config;
  },
};

export default nextConfig;
```

> **Note:** Add every AI library you instrument to the `externals` array (e.g. `"openai"`, `"@anthropic-ai/sdk"`, etc.). This ensures webpack does not bundle them, allowing the SDK's monkey-patching to work correctly.

If auto-instrumentation still doesn't work, pass modules explicitly via `instrumentModules`:

```ts
// instrumentation.ts
import OpenAI from "openai";
import { Flowlines } from "@flowlines/sdk";

export function register() {
  Flowlines.init({
    apiKey: process.env.FLOWLINES_API_KEY!,
    instrumentModules: { openAI: OpenAI },
  });
}
```

See [Selecting which libraries to instrument](#selecting-which-libraries-to-instrument) for the full list of supported keys.

### Existing OpenTelemetry setup

If your application **already has its own OpenTelemetry `NodeSDK`**, set `hasExternalOtel: true` so Flowlines does not create a second one. You then compose Flowlines' span processor and instrumentations into your existing setup.

```ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { Flowlines } from "@flowlines/sdk";

Flowlines.init({
  apiKey: "your_flowlines_api_key",
  hasExternalOtel: true,
});

const sdk = new NodeSDK({
  // Add the Flowlines span processor alongside your own processors
  spanProcessors: [
    yourExistingProcessor,
    Flowlines.createSpanProcessor(),
  ],
  // Add LLM instrumentations alongside your own instrumentations
  instrumentations: [
    ...yourExistingInstrumentations,
    ...Flowlines.getInstrumentations(),
  ],
});

sdk.start();
```

With this setup, your existing observability pipeline continues to work as before, and Flowlines receives only the LLM-related spans.

When managing your own `NodeSDK`, shut down the Flowlines instance separately:

```ts
await sdk.shutdown();
await Flowlines.shutdown();
```

## Library-Specific Notes

### Selecting which libraries to instrument

By default, the SDK instruments all supported libraries. To instrument only specific ones, pass `instrumentModules`:

```ts
import OpenAI from "openai";
import { Flowlines } from "@flowlines/sdk";

Flowlines.init({
  apiKey: "your_flowlines_api_key",
  instrumentModules: {
    openAI: OpenAI,
  },
});

const openai = new OpenAI();
```

Supported keys: `openAI`, `anthropic`, `cohere`, `bedrock`, `google_vertexai`, `google_aiplatform`, `google_generativeai`, `pinecone`, `together`, `chromadb`, `qdrant`, `langchain`, `llamaIndex`, `mcp`.

> **Import style matters.** The required import style depends on the library:
>
> - **OpenAI** — pass the **default import** (the class): `import OpenAI from "openai"` → `{ openAI: OpenAI }`
> - **Anthropic** — pass a **namespace import**: `import * as AnthropicModule from "@anthropic-ai/sdk"` → `{ anthropic: AnthropicModule }`
>
> Using the wrong import style will cause instrumentation to silently fail or throw errors.

### Instrumenting Anthropic

To instrument the Anthropic SDK, you must import the module as a **namespace** (`import * as ...`) and pass it to `instrumentModules`:

```ts
import Anthropic from "@anthropic-ai/sdk";
import * as AnthropicModule from "@anthropic-ai/sdk"; // namespace import required for instrumentation
import { Flowlines } from "@flowlines/sdk";

// Initialize Flowlines BEFORE creating the Anthropic client
Flowlines.init({
  apiKey: "your_flowlines_api_key",
  instrumentModules: { anthropic: AnthropicModule },
});

// Now create the client and make calls as usual
const client = new Anthropic();

await Flowlines.context({ userId: "user-123", sessionId: "sess-456" }, async () => {
  const response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello!" }],
  });
});
```

> **Why the namespace import?** The SDK patches the module's exports to add tracing. A namespace import (`import * as AnthropicModule`) gives the SDK access to the module object so it can instrument exported functions and classes.

## Configuration Reference

| Option | Type | Default | Description |
|---|---|---|---|
| `apiKey` | `string` | **(required)** | Flowlines API key |
| `ingestEndpoint` | `string` | `https://ingest.flowlines.ai` | Flowlines ingest endpoint URL. HTTPS required (HTTP allowed on localhost only) |
| `apiEndpoint` | `string` | `https://api.flowlines.ai` | Flowlines API endpoint URL (used by `getMemory`, `endSession`). HTTPS required (HTTP allowed on localhost only) |
| `hasExternalOtel` | `boolean` | `false` | Set to `true` when using your own OpenTelemetry `NodeSDK` |
| `instrumentModules` | `InstrumentModules` | `undefined` | Restrict instrumentation to specific libraries. When omitted, all supported libraries are instrumented |
| `exportIntervalMs` | `number` | `5000` | Batch export interval in milliseconds |
| `maxQueueSize` | `number` | `2048` | Maximum number of spans queued before dropping |
| `maxExportBatchSize` | `number` | `512` | Maximum number of spans per export batch |
| `verbose` | `boolean` | `false` | Enable verbose debug logging to console |

## Supported Libraries

OpenAI, Anthropic, Cohere, AWS Bedrock, Google Vertex AI, Google AI Platform, Google Generative AI, Pinecone, Together AI, ChromaDB, Qdrant, LangChain, LlamaIndex, MCP.

## Examples

The [`examples/`](./examples) directory contains runnable demos:

- **[Anthropic chat](./examples/anthropic/)** — Interactive multi-turn CLI chat with Claude Haiku, featuring tool use, `Flowlines.context()` for per-request metadata, and graceful shutdown.

## Requirements

- Node.js >= 20
- ESM or CommonJS

## Troubleshooting

### No spans appearing in Flowlines

1. **Check your API key.** The constructor throws if `apiKey` is empty. Make sure you are passing a valid key.
2. **Flowlines must be initialized before LLM clients.** The SDK instruments libraries by monkey-patching module imports. If you create an OpenAI/Anthropic client *before* calling `Flowlines.init()`, those calls will not be traced. Always initialize Flowlines first.
3. **Check that spans are being generated.** The SDK only exports spans that have `gen_ai.*` or `ai.*` attributes. If your library does not produce these attributes, spans will be filtered out.

### `Error: Insecure Flowlines endpoint`

The `ingestEndpoint` and `apiEndpoint` options require HTTPS. HTTP is only allowed for `localhost` and `127.0.0.1` (for local development).

### Duplicate spans / instrumentation conflicts

If you use `hasExternalOtel: true`, make sure you are not registering the same instrumentations twice (once in your `NodeSDK` and once via `Flowlines.getInstrumentations()`). Use one or the other for LLM libraries.

### Spans are delayed

Spans are batched before export. By default, the batch interval is 5 seconds (`exportIntervalMs`). If you need spans sooner, lower this value. During shutdown, call `Flowlines.shutdown()` to force-flush all pending spans.