Objects

Objects are visual components that tools can create. When a tool creates an object, it shows up as a tab in the UI where users can interact with it.

What Objects Do

Tools return data. But sometimes you want more than raw data - you want a visual representation. A spreadsheet. A chart. A document editor.

Objects solve this. They're persistent entities with custom renderers. A tool creates an object, and the UI renders it using your custom code.

Defining Objects

Objects are defined in objects.json:

[
  {
    "name": "note-editor",
    "title": "Note",
    "renders": ["web"],
    "capabilities": ["storage"],
    "metadata_schema": {
      "type": "object",
      "properties": {
        "noteId": {
          "type": "string",
          "description": "ID of the note to display"
        }
      },
      "required": ["noteId"]
    }
  }
]

Object Fields

Field Required What It Does
name Yes Identifier (kebab-case)
title Yes Display name in UI tabs
renders Yes Where to render: ["web"], ["cli"], or both
capabilities No What the renderer can access
lifecycle No Enable update/unmount hooks
metadata_schema Yes Schema for object references

Metadata vs Data

Objects have metadata and data. They're different:

The metadata_schema defines references, not actual content. Your renderer fetches the real data:

{
  "metadata_schema": {
    "type": "object",
    "properties": {
      "noteId": { "type": "string" }
    }
  }
}

This stores just the noteId. The renderer uses that ID to fetch the actual note content.

Creating Objects

Tools with the object capability can create objects:

export default async function create_note(
  input: Input,
  capabilities: Capabilities
): Promise<Output> {
  const noteId = crypto.randomUUID();
  
  // Save the actual note data to storage
  await capabilities.storage
    .use('@my-org/notes')
    .put(`/notes/${noteId}.json`, {
      title: input.title,
      content: input.content
    });
  
  // Create an object that references this note
  await capabilities.object.set('@my-org/notes', {
    type: 'note-editor',
    name: input.title,
    metadata: { noteId }
  });
  
  return { noteId, success: true };
}

The object appears as a tab in the UI. When users click it, your renderer loads the actual data.

Building Renderers

Renderers are TypeScript files that return DOM elements. Create them in src/objects/{name}/web.ts:

// src/objects/note-editor/web.ts
import { ObjectInfo, Capabilities, ObjectLifecycle } from './types.js';

export default async function web(
  info: ObjectInfo,
  capabilities: Capabilities,
  lifecycle: ObjectLifecycle
): Promise<HTMLElement> {
  const container = document.createElement('div');
  container.className = 'note-editor';
  
  // Fetch the actual note data
  const note = await capabilities.storage
    .use('@my-org/notes')
    .get(`/notes/${info.metadata.noteId}.json`);
  
  container.innerHTML = `
    <h1>${note.title}</h1>
    <div class="content">${note.content}</div>
  `;
  
  return container;
}

Renderer Capabilities

Renderers can access capabilities declared in objects.json:

Capability What It Provides
storage Read/write to storage
tool Call tools from the renderer
ai Access AI completions
// Using the tool capability
const result = await capabilities.callTool('@my-org/notes', 'update_note', {
  noteId: info.metadata.noteId,
  content: newContent
});

Lifecycle Hooks

Enable lifecycle hooks for dynamic updates:

{
  "name": "note-editor",
  "lifecycle": true
}

Then use them in your renderer:

export default async function web(
  info: ObjectInfo,
  capabilities: Capabilities,
  lifecycle: ObjectLifecycle
): Promise<HTMLElement> {
  const container = document.createElement('div');
  
  // Called when object data changes
  lifecycle.onDataUpdated((newMetadata) => {
    refreshContent(container, newMetadata);
  });
  
  // Called when object is removed from view
  lifecycle.onUnmount(() => {
    cleanup();
  });
  
  return container;
}

Updating Objects

Tools can update existing objects:

// Update an existing object
await capabilities.object.set('@my-org/notes', {
  type: 'note-editor',
  id: existingObjectId,  // Pass the existing ID
  name: 'Updated Title',
  metadata: { noteId }
});

If you pass an id, the object is updated. If you don't, a new object is created.

Deleting Objects

Remove objects when they're no longer needed:

await capabilities.object.delete('@my-org/notes', {
  type: 'note-editor',
  id: objectId
});

Cross-App Objects

Objects can be stored in another app's namespace for team sharing:

{
  "name": "shared-doc",
  "storage": {
    "inApp": "@my-org/shared",
    "path": "/teams/<token.teamId>/objects/",
    "tokenType": "team",
    "tokenFromApp": "@my-org/auth"
  }
}

This stores objects in @my-org/shared storage, scoped by team.

Example: Table Viewer

Here's a complete example of a table object:

objects.json:

[
  {
    "name": "table-view",
    "title": "Table",
    "renders": ["web"],
    "capabilities": ["storage", "tool"],
    "lifecycle": true,
    "metadata_schema": {
      "type": "object",
      "properties": {
        "tableId": { "type": "string" }
      },
      "required": ["tableId"]
    }
  }
]

src/objects/table-view/web.ts:

export default async function web(
  info: ObjectInfo,
  capabilities: Capabilities,
  lifecycle: ObjectLifecycle
): Promise<HTMLElement> {
  const container = document.createElement('div');
  
  async function render() {
    const data = await capabilities.storage
      .use('@my-org/tables')
      .get(`/tables/${info.metadata.tableId}.json`);
    
    container.innerHTML = `
      <table>
        <thead>
          <tr>${data.columns.map(c => `<th>${c}</th>`).join('')}</tr>
        </thead>
        <tbody>
          ${data.rows.map(row => 
            `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`
          ).join('')}
        </tbody>
      </table>
    `;
  }
  
  await render();
  
  lifecycle.onDataUpdated(() => render());
  
  return container;
}

Objects turn your tools from data-in-data-out functions into full interactive experiences.