TypeScript SDK

The official TypeScript SDK for CntrlNode. Works in Node.js, Bun, Deno, and browser environments.

Install

npm install @cntrlnode/sdk
# or
pnpm add @cntrlnode/sdk
# or
yarn add @cntrlnode/sdk

Connect

import { CntrlNodeClient } from '@cntrlnode/sdk'

const client = new CntrlNodeClient({
  baseUrl: 'http://localhost:7474',
  apiKey: 'your-api-key', // optional — omit if auth is disabled
})

State Store

// Set a value (with optional TTL in seconds)
await client.state.set('workflow-1', 'results', { summary: 'Q4 up 12%' }, 3600)

// Get a value
const { value, version } = await client.state.get('workflow-1', 'results')

// Delete
await client.state.delete('workflow-1', 'results')

// List all keys in a workflow namespace
const { keys } = await client.state.list('workflow-1')

// Optimistic write (throws VERSION_CONFLICT on race)
await client.state.setWithVersion('workflow-1', 'counter', value + 1, version)

// Subscribe to changes (SSE)
const es = client.state.subscribe('workflow-1', 'results', (event) => {
  console.log('changed:', event.newValue, 'v' + event.version)
})
es.close() // unsubscribe

Task Bus

// Submit a task
const task = await client.tasks.submit({
  workflowId: 'workflow-1',
  agentId: 'researcher-1',
  payload: { query: 'Summarise Q4 report' },
})

// Submit a child task
const child = await client.tasks.submit({
  workflowId: 'workflow-1',
  agentId: 'writer-1',
  parentId: task.id,
  payload: { draft: true },
})

// Idempotent submission (safe to call multiple times)
const task = await client.tasks.submit({
  workflowId: 'workflow-1',
  agentId: 'researcher-1',
  idempotencyKey: 'research-q4-2024',
  payload: {},
})

// Get status
const t = await client.tasks.get(task.id)
console.log(t.status) // SUBMITTED | ACCEPTED | IN_PROGRESS | COMPLETED | FAILED | CANCELLED

// List all tasks in a workflow
const { tasks } = await client.tasks.list('workflow-1')

// Cancel (recursively cancels all child tasks)
await client.tasks.cancel(task.id)

// Lifecycle (called by the receiving agent)
await client.tasks.accept(task.id)
await client.tasks.complete(task.id, { result: 'done' })
await client.tasks.fail(task.id, 'Connection timed out')

Agent Registry

// Register
await client.registry.register({
  id: 'researcher-1',
  tags: ['research', 'web-search', 'pdf'],
  model: 'claude-sonnet-4-5',
  endpoint: 'http://my-agent:8080',
  maxConcurrency: 3,
})

// Heartbeat (keep the agent healthy)
await client.registry.heartbeat('researcher-1')

// Auto-heartbeat every 15s
const stop = client.registry.startHeartbeat('researcher-1', 15_000)
// Call stop() to cancel

// Discover by tags (must match ALL tags)
const { agents } = await client.registry.discover({ tags: ['research', 'pdf'] })

// Semantic discovery (requires Ollama)
const { agents } = await client.registry.discover({
  query: 'agent that reads PDFs and extracts tables',
  topK: 3,
})

// List all agents
const { agents } = await client.registry.list()

// Get one agent
const agent = await client.registry.get('researcher-1')

// Deregister
await client.registry.deregister('researcher-1')

Complete agent example

import { CntrlNodeClient } from '@cntrlnode/sdk'

const client = new CntrlNodeClient({ baseUrl: 'http://localhost:7474' })

async function runAgent() {
  // Register and start heartbeat
  await client.registry.register({
    id: 'my-agent',
    tags: ['summarize'],
  })
  const stopHeartbeat = client.registry.startHeartbeat('my-agent', 15_000)

  // Poll for tasks (in production, use a webhook or SSE)
  setInterval(async () => {
    const { tasks } = await client.tasks.list('workflow-1')
    const mine = tasks.filter(t => t.agentId === 'my-agent' && t.status === 'SUBMITTED')

    for (const task of mine) {
      await client.tasks.accept(task.id)

      try {
        const result = await doWork(task.payload)
        await client.tasks.complete(task.id, result)
      } catch (err) {
        await client.tasks.fail(task.id, err.message)
      }
    }
  }, 2000)
}

Types

interface Task {
  id: string
  workflowId: string
  agentId: string
  parentId?: string
  payload: unknown
  status: 'SUBMITTED' | 'ACCEPTED' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'CANCELLED'
  result?: unknown
  error?: string
  createdAt: string
  updatedAt: string
}

interface Agent {
  id: string
  tags: string[]
  model?: string
  endpoint?: string
  maxConcurrency?: number
  healthy: boolean
  registeredAt: string
  lastHeartbeat: string
}

interface StateEntry {
  value: unknown
  version: number
}