Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iii-hq/sdk/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Stream API provides a real-time, key-value data store with automatic synchronization across workers. Unlike traditional databases, Stream data is organized into named streams with groups and items, making it ideal for collaborative applications, live dashboards, and multiplayer experiences.

Stream Structure

Hierarchy:
  • Stream: Named collection (e.g., “todos”, “users”, “messages”)
  • Group: Logical partition within stream (e.g., “inbox”, “team-1”, “room-42”)
  • Item: Individual record with unique ID and data
Streams are automatically created on first use. No schema definition required.

Stream Interface

The IStream<TData> interface defines operations:
// Source: packages/node/iii/src/stream.ts:111-118
interface IStream<TData> {
  get(input: StreamGetInput): Promise<TData | null>
  set(input: StreamSetInput): Promise<StreamSetResult<TData> | null>
  delete(input: StreamDeleteInput): Promise<DeleteResult>
  list(input: StreamListInput): Promise<TData[]>
  listGroups(input: StreamListGroupsInput): Promise<string[]>
  update(input: StreamUpdateInput): Promise<StreamUpdateResult<TData> | null>
}

Creating Streams

Custom Stream Implementation

Implement your own backend:
import type { IStream } from 'iii-sdk'

class RedisStream<TData> implements IStream<TData> {
  constructor(private redis: RedisClient, private streamName: string) {}
  
  async get(input: StreamGetInput): Promise<TData | null> {
    const key = `${input.stream_name}:${input.group_id}:${input.item_id}`
    const data = await this.redis.get(key)
    return data ? JSON.parse(data) : null
  }
  
  async set(input: StreamSetInput): Promise<StreamSetResult<TData>> {
    const key = `${input.stream_name}:${input.group_id}:${input.item_id}`
    const old_value = await this.get(input)
    await this.redis.set(key, JSON.stringify(input.data))
    return { old_value, new_value: input.data }
  }
  
  async delete(input: StreamDeleteInput): Promise<DeleteResult> {
    const old_value = await this.get(input)
    const key = `${input.stream_name}:${input.group_id}:${input.item_id}`
    await this.redis.del(key)
    return { old_value }
  }
  
  async list(input: StreamListInput): Promise<TData[]> {
    const pattern = `${input.stream_name}:${input.group_id}:*`
    const keys = await this.redis.keys(pattern)
    const values = await Promise.all(
      keys.map(key => this.redis.get(key))
    )
    return values.map(v => JSON.parse(v))
  }
  
  async listGroups(input: StreamListGroupsInput): Promise<string[]> {
    const pattern = `${input.stream_name}:*`
    const keys = await this.redis.keys(pattern)
    const groups = new Set(
      keys.map(key => key.split(':')[1])
    )
    return Array.from(groups)
  }
  
  async update(input: StreamUpdateInput): Promise<StreamUpdateResult<TData>> {
    const current = await this.get(input)
    if (!current) return null
    
    let updated = { ...current }
    for (const op of input.ops) {
      updated = applyUpdateOp(updated, op)
    }
    
    return await this.set({ ...input, data: updated })
  }
}

const stream = new RedisStream<Todo>(redisClient, 'todos')
iii.createStream('todos', stream)

Registering Stream Functions

The SDK registers stream functions automatically:
// Source: packages/node/iii/src/iii.ts:382-391
createStream = <TData>(streamName: string, stream: IStream<TData>): void => {
  this.registerFunction({ id: `stream::get(${streamName})` }, stream.get.bind(stream))
  this.registerFunction({ id: `stream::set(${streamName})` }, stream.set.bind(stream))
  this.registerFunction({ id: `stream::delete(${streamName})` }, stream.delete.bind(stream))
  this.registerFunction({ id: `stream::list(${streamName})` }, stream.list.bind(stream))
  this.registerFunction(
    { id: `stream::list_groups(${streamName})` },
    stream.listGroups.bind(stream)
  )
}
Functions are named: stream::get(streamName), stream::set(streamName), etc.

Stream Operations

Get Item

Retrieve a single item by ID:
// Input types
type StreamGetInput = {
  stream_name: string
  group_id: string
  item_id: string
}

// Usage
const todo = await streams.get<Todo>('todos', 'inbox', 'todo-123')

if (todo) {
  console.log('Description:', todo.description)
} else {
  console.log('Todo not found')
}
Type definition:
// Source: packages/node/iii/src/stream.ts:27-31
type StreamGetInput = {
  stream_name: string
  group_id: string
  item_id: string
}

Set Item

Create or update an item:
type StreamSetInput = {
  stream_name: string
  group_id: string
  item_id: string
  data: any
}

type StreamSetResult<TData> = {
  old_value?: TData    // Previous value if item existed
  new_value: TData     // Current value after set
}

const result = await streams.set<Todo>('todos', 'inbox', 'todo-123', {
  id: 'todo-123',
  description: 'Buy groceries',
  groupId: 'inbox',
  createdAt: new Date().toISOString(),
  completedAt: null
})

console.log('Previous value:', result.old_value)
console.log('New value:', result.new_value)
Type definitions:
// Source: packages/node/iii/src/stream.ts:33-39, 56-59
type StreamSetInput = {
  stream_name: string
  group_id: string
  item_id: string
  data: any
}

type StreamSetResult<TData> = {
  old_value?: TData
  new_value: TData
}

Delete Item

Remove an item:
type StreamDeleteInput = {
  stream_name: string
  group_id: string
  item_id: string
}

type DeleteResult = {
  old_value?: any  // Value before deletion
}

const result = await streams.delete('todos', 'inbox', 'todo-123')

if (result.old_value) {
  console.log('Deleted todo:', result.old_value.description)
} else {
  console.log('Todo did not exist')
}
Type definitions:
// Source: packages/node/iii/src/stream.ts:41-45, 97-100
type StreamDeleteInput = {
  stream_name: string
  group_id: string
  item_id: string
}

type DeleteResult = {
  old_value?: any
}

List Items in Group

Retrieve all items in a group:
type StreamListInput = {
  stream_name: string
  group_id: string
}

const todos = await streams.list<Todo>('todos', 'inbox')

console.log(`Found ${todos.length} todos in inbox`)
todos.forEach(todo => {
  console.log(`- ${todo.description}`)
})
Type definition:
// Source: packages/node/iii/src/stream.ts:47-50
type StreamListInput = {
  stream_name: string
  group_id: string
}

List Groups

Retrieve all group IDs in a stream:
type StreamListGroupsInput = {
  stream_name: string
}

const groups = await streams.listGroups('todos')

console.log('Todo groups:', groups)  // ["inbox", "completed", "archived"]
Type definition:
// Source: packages/node/iii/src/stream.ts:52-54
type StreamListGroupsInput = {
  stream_name: string
}

Partial Updates

Update specific fields without fetching the entire item:

Update Operations

type UpdateOp =
  | UpdateSet        // Set field value
  | UpdateIncrement  // Increment number
  | UpdateDecrement  // Decrement number
  | UpdateRemove     // Remove field
  | UpdateMerge      // Merge object
Type definitions:
// Source: packages/node/iii/src/stream.ts:66-102
type UpdateSet = {
  type: 'set'
  path: string
  value: any
}

type UpdateIncrement = {
  type: 'increment'
  path: string
  by: number
}

type UpdateDecrement = {
  type: 'decrement'
  path: string
  by: number
}

type UpdateRemove = {
  type: 'remove'
  path: string
}

type UpdateMerge = {
  type: 'merge'
  path: string
  value: any
}

Set Field

await streams.update('todos', 'inbox', 'todo-123', [
  { type: 'set', path: 'description', value: 'Updated description' },
  { type: 'set', path: 'priority', value: 'high' }
])

Increment/Decrement

await streams.update('counters', 'global', 'page-views', [
  { type: 'increment', path: 'count', by: 1 }
])

await streams.update('inventory', 'warehouse-1', 'widget-123', [
  { type: 'decrement', path: 'quantity', by: 5 }
])

Remove Field

await streams.update('todos', 'inbox', 'todo-123', [
  { type: 'remove', path: 'dueDate' }
])

Merge Object

await streams.update('users', 'active', 'user-456', [
  {
    type: 'merge',
    path: 'preferences',
    value: {
      theme: 'dark',
      notifications: true
    }
  }
])

Multiple Operations

const result = await streams.update<Todo>('todos', 'inbox', 'todo-123', [
  { type: 'set', path: 'completedAt', value: new Date().toISOString() },
  { type: 'set', path: 'status', value: 'completed' },
  { type: 'increment', path: 'completionCount', by: 1 }
])

console.log('Old value:', result.old_value)
console.log('New value:', result.new_value)
Update input type:
// Source: packages/node/iii/src/stream.ts:104-109
type StreamUpdateInput = {
  stream_name: string
  group_id: string
  item_id: string
  ops: UpdateOp[]
}

Real-World Example: Todo Application

// Source: packages/node/iii-example/src/index.ts:7-130 (excerpts)
import { useApi } from './hooks'
import { streams } from './stream'
import type { Todo } from './types'

type Todo = {
  id: string
  description: string
  groupId: string
  createdAt: string
  dueDate?: string
  completedAt?: string | null
}

// Create todo
useApi(
  {
    api_path: '/todo',
    http_method: 'POST',
    description: 'Create a new todo'
  },
  async (req, ctx) => {
    ctx.logger.info('Creating new todo', { body: req.body })
    
    const { description, dueDate } = req.body
    const todoId = `todo-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
    
    if (!description) {
      return { status_code: 400, body: { error: 'Description is required' } }
    }
    
    const newTodo: Todo = {
      id: todoId,
      description,
      groupId: 'inbox',
      createdAt: new Date().toISOString(),
      dueDate: dueDate,
      completedAt: null
    }
    const todo = await streams.set<Todo>('todo', 'inbox', todoId, newTodo)
    
    return { status_code: 201, body: todo }
  }
)

// Update todo
useApi(
  {
    api_path: 'todo/:id',
    http_method: 'PUT',
    description: 'Update a todo'
  },
  async (req, ctx) => {
    const todoId = req.path_params.id
    const existingTodo = todoId ? await streams.get<Todo | null>('todo', 'inbox', todoId) : null
    
    ctx.logger.info('Updating todo', { body: req.body, todoId })
    
    if (!existingTodo) {
      ctx.logger.error('Todo not found')
      return { status_code: 404, body: { error: 'Todo not found' } }
    }
    
    const todo = await streams.set<Todo>('todo', 'inbox', todoId, { ...existingTodo, ...req.body })
    
    ctx.logger.info('Todo updated successfully', { todoId })
    
    return { status_code: 200, body: todo }
  }
)

// Delete todo
useApi(
  {
    api_path: 'todo',
    http_method: 'DELETE',
    description: 'Delete a todo'
  },
  async (req, ctx) => {
    const { todoId } = req.body
    
    ctx.logger.info('Deleting todo', { body: req.body })
    
    if (!todoId) {
      ctx.logger.error('todoId is required')
      return { status_code: 400, body: { error: 'todoId is required' } }
    }
    
    await streams.delete('todo', 'inbox', todoId)
    
    ctx.logger.info('Todo deleted successfully', { todoId })
    
    return { status_code: 200, body: { success: true } }
  }
)

Stream Helper Wrapper

Create a type-safe helper for specific streams:
class TodoStream {
  constructor(private streams: StreamAPI) {}
  
  async create(todo: Omit<Todo, 'id' | 'createdAt'>): Promise<Todo> {
    const id = `todo-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
    const fullTodo: Todo = {
      ...todo,
      id,
      createdAt: new Date().toISOString(),
      completedAt: null
    }
    
    const result = await this.streams.set<Todo>('todos', todo.groupId, id, fullTodo)
    return result.new_value
  }
  
  async get(groupId: string, id: string): Promise<Todo | null> {
    return this.streams.get<Todo>('todos', groupId, id)
  }
  
  async list(groupId: string): Promise<Todo[]> {
    return this.streams.list<Todo>('todos', groupId)
  }
  
  async complete(groupId: string, id: string): Promise<Todo> {
    const result = await this.streams.update<Todo>('todos', groupId, id, [
      { type: 'set', path: 'completedAt', value: new Date().toISOString() }
    ])
    return result!.new_value
  }
  
  async delete(groupId: string, id: string): Promise<void> {
    await this.streams.delete('todos', groupId, id)
  }
}

const todos = new TodoStream(streams)

// Type-safe API
const todo = await todos.create({
  description: 'Buy groceries',
  groupId: 'inbox',
  dueDate: '2024-03-15'
})

const completed = await todos.complete('inbox', todo.id)

Stream vs State

The III SDK provides both Stream and State APIs:
AspectStreamState
Structurestream → group → itemscope → key
Hierarchy3 levels2 levels
Use CaseCollaborative data, grouped recordsSimple key-value storage
GroupingBuilt-in with group_idManual with key prefixes
List APIlist(group), listGroups()Depends on implementation
State example:
import { state } from 'iii-sdk'

// Set state
await state.set({ scope: 'todos', key: 'todo-123', data: todoData })

// Get state
const todo = await state.get<Todo>({ scope: 'todos', key: 'todo-123' })

// Delete state
await state.delete({ scope: 'todos', key: 'todo-123' })
Use Stream when you need grouping and listing. Use State for simple key-value storage.

Backend Implementation Notes

When implementing IStream:
1

Handle Nulls

Return null from get() when item doesn’t exist. Return null from update() if item doesn’t exist.
2

Atomic Updates

Implement update() atomically to prevent race conditions. Use database transactions or compare-and-swap.
3

Group Listing

listGroups() should return unique group IDs. Consider caching for large streams.
4

Serialization

Stream data is passed as JSON. Ensure your types serialize correctly.
5

Error Handling

Throw descriptive errors. The Engine will convert them to InvocationResult errors.

Multi-Language Support

import type { IStream } from 'iii-sdk'

const stream: IStream<MyData> = new CustomStream()
iii.createStream('mydata', stream)

const item = await streams.get<MyData>('mydata', 'group1', 'item1')

Next Steps

Channels

Stream large binary data with channels

Functions

Call stream operations from functions

Build docs developers (and LLMs) love