User Guide mdi-view-dashboard Guide Contents

Welcome to DSiloed

mdi-link

AI Enabler Built on Enterprise Data Models

DSiloed is a powerful AI enablement platform built on comprehensive enterprise data models. It provides everything you need to build intelligent, AI-powered applications with rich memory support, advanced chat features, and deep integration with leading AI providers.

Whether you're building customer service bots, intelligent automation systems, or knowledge management platforms, DSiloed gives you enterprise-grade data infrastructure combined with cutting-edge AI capabilities including persistent memory graphs, real-time chat, document processing, and autonomous agents.

This interactive guide will walk you through getting started with DSiloed, setting up AI models, leveraging memory systems, and building powerful AI-enabled enterprise applications.

Core Architecture

DSiloed is built on a flexible, enterprise-grade data model designed for extensibility and integration. Here are the key components:

Party Model

  • People and Organizations - Represented as Parties
  • Flexible Relationships - Connect parties with typed relationships
  • Role-based - Assign roles for different contexts

Business Transaction Events

  • Event-based Transactions - Track business events
  • Typed Event Relations - Connect related events
  • Party Roles in Events - Associates parties with roles in events

Security System

  • Capabilities - Fine-grained permissions
  • Security Roles - Group capabilities for assignment
  • Tenant Isolation - Multi-tenant architecture

AI & Memory System

  • Rich Memory Graphs - Persistent knowledge with pattern recognition
  • Real-time Chat - WebSocket-based conversations with AI models
  • Document Processing - PDF, DOCX, Markdown, and image support
  • Autonomous Agents - Self-improving AI with tool execution
AI-First, API-Driven Platform

DSiloed is designed with an AI-first, API-driven approach. Every AI capability—from chat conversations to memory graphs to autonomous agents—is accessible through our comprehensive REST API and WebSocket connections. Build any AI-powered interface on top of our enterprise data foundation.

All sample applications in this demo are built using static JavaScript frameworks that interact with the API, demonstrating how you can create sophisticated AI-enabled applications with standard web technologies.

Quick Start

Ready to build with AI? Head to the Getting Started section to create your first tenant, or jump to Setting up AI to connect your first AI model. Check out Sample Apps to see real-world AI applications in action.

Getting Started

mdi-link
Create Your Tenant

To get started with DSiloed, you need to create a tenant. A tenant is an isolated environment for your application data. Follow these steps to create your first tenant:

Visit the tenant signup page.

Fill in the following information:

  • Tenant Name: A unique name for your tenant
  • Admin Email: Your email address for admin access
  • Password: Set a secure password

Click "Create Tenant" to complete your registration.

Your tenant will be created, and you'll be redirected to the dashboard where you can access all the available applications.

Note: Each tenant is isolated by row based security, so each tenant's data is isolated

Exploring the Dashboard

After creating your tenant, you'll be directed to the dashboard where you can access all available applications. Here's what you'll find:

{{ app.name }}

{{ app.description }}

{{ feature }}
Open App

Setting up AI with LLM Manager

mdi-link
Configure LLM Models for AI Features mdi-link

DSiloed includes powerful AI capabilities through integration with Large Language Models (LLMs). To enable AI features in your applications, you need to configure LLM models using the LLM Manager application.

This guide will walk you through setting up LLM models from various providers including Anthropic (Claude), OpenAI (GPT), Google (Gemini), xAI (Grok), and local Ollama installations.

Step 1: Choose Your LLM Provider and Get API Key mdi-link

DSiloed supports multiple LLM providers. Choose one of the following options based on your needs:

Option 1: Anthropic (Claude)

First, you'll need to obtain an API key from Anthropic to access Claude models:

Go to console.anthropic.com and sign up for an account if you don't have one.

Navigate to the API Keys section and create a new key:

  • Click "Create Key"
  • Give it a descriptive name (e.g., "DSiloed Integration")
  • Copy the generated API key (it starts with "sk-ant-")
Important: Keep your API key secure and never share it publicly. You'll only see the full key once when it's created.

Add credits to your Anthropic account to enable API usage. Even a small amount ($5-10) will provide substantial usage for development and testing.

Option 2: OpenAI (GPT)

To use OpenAI's GPT models (GPT-3.5, GPT-4), you'll need an OpenAI API key:

Go to platform.openai.com and sign up or log in to your account.

Navigate to API Keys section in your account settings:

  • Click "Create new secret key"
  • Name your key (e.g., "DSiloed Integration")
  • Copy the API key (it starts with "sk-")
Important: Save your API key securely. You won't be able to see it again.

Add credits to your OpenAI account. OpenAI uses a pay-as-you-go pricing model with different rates for each model.

Option 3: Google (Gemini)

To use Google's Gemini models, obtain an API key from Google AI Studio:

Go to makersuite.google.com and sign in with your Google account.

Create a new API key:

  • Click "Create API Key"
  • Select your project or create a new one
  • Copy the generated API key
Note: Google Gemini API has a free tier with generous limits for development.

Make sure the Generative Language API is enabled in your Google Cloud Console for your project.

Option 4: xAI (Grok)

To use xAI's Grok models, you'll need an API key from xAI:

Go to console.x.ai and sign up or log in to your account.

Navigate to the API Keys section and create a new key:

  • Click on "API Keys" in the navigation
  • Click "Create API key"
  • Name your key (e.g., "DSiloed Integration")
  • Copy the generated API key (it starts with "xai-")
Important: Store your API key securely. You won't be able to view the full key again after creation.

xAI provides $25 in free credits every month. You can add additional credits through the billing section if needed. The free tier is generous for development and testing.

Note: Grok models are designed for real-time information access and have strong reasoning capabilities.
Option 5: Local Ollama

Ollama allows you to run LLMs locally on your own hardware, providing privacy and offline capability:

Download and install Ollama from ollama.ai for your operating system.

# macOS/Linux
curl -fsSL https://ollama.ai/install.sh | sh

# Windows - Download installer from ollama.ai

Pull the models you want to use:

# Popular models
ollama pull llama2
ollama pull mistral
ollama pull codellama
ollama pull mixtral
Tip: Start with smaller models like Mistral (4GB) if you have limited RAM.

Ollama runs as a local API server on port 11434. It starts automatically after installation. No API key is needed for local usage.

# Check if Ollama is running
ollama list
Step 2: Configure LLM Model in LLM Manager mdi-link

Once you have your API key (or Ollama installed), use the LLM Manager application to create an LLM Model by selecting from pre-configured model types:

From your dashboard, click on mdi-smart-toyLLM Manager to open the LLM management application.

In the LLM Manager, click "Create LLM Model" to open the model creation dialog. You'll see provider cards for each available LLM provider. Select a provider to view their available models:

Select a provider to view available models with their specifications:

Anthropic OpenAI Google xAI Ollama

Available Anthropic Models:

Model Description Context Window Pricing
claude-opus-4-20250514 Claude Opus 4.1 - Most powerful model 200K tokens $15/$75 per M tokens
claude-opus-4-20241129 Claude Opus 4 - Previous generation flagship 200K tokens $15/$75 per M tokens
claude-sonnet-4-20250514 Claude Sonnet 4 - Balanced performance 200K tokens $3/$15 per M tokens
claude-3-7-sonnet-20250219 Claude 3.7 Sonnet - Enhanced reasoning 200K tokens $3/$15 per M tokens
claude-3-5-haiku-20241022 Claude 3.5 Haiku - Fast and efficient 200K tokens $0.80/$4 per M tokens
claude-3-haiku-20240307 Claude 3 Haiku - Budget-friendly 200K tokens $0.25/$1.25 per M tokens
Setup: Select a model card, enter your Anthropic API key (sk-ant-...), and optionally customize the internal identifier

Available OpenAI Models:

Model Description Context Window Pricing
gpt-4o GPT-4 Optimized - Best general-purpose 128K tokens $2.50/$10 per M tokens
gpt-4o-mini GPT-4 Optimized Mini - Compact and efficient 128K tokens $0.15/$0.60 per M tokens
o1 O1 - Advanced reasoning 200K tokens $15/$60 per M tokens
o1-mini O1 Mini - Reasoning optimized 128K tokens $3/$12 per M tokens
o1-preview O1 Preview - Beta reasoning model 128K tokens $15/$60 per M tokens
gpt-4-turbo GPT-4 Turbo - Previous generation flagship 128K tokens $10/$30 per M tokens
gpt-3.5-turbo GPT-3.5 Turbo - Legacy fast model 16K tokens $0.50/$1.50 per M tokens
Setup: Select a model card, enter your OpenAI API key (sk-...), and optionally customize the internal identifier

Available Google Models:

Model Description Context Window Pricing
gemini-2.5-flash-lite-latest Gemini 2.5 Flash Lite - Ultra-fast 1M tokens $0.075/$0.30 per M tokens
gemini-2.5-flash-latest Gemini 2.5 Flash - Speed optimized 1M tokens $0.15/$0.60 per M tokens
gemini-2.5-pro-latest Gemini 2.5 Pro - Most capable 2M tokens $1.25/$5 per M tokens
Setup: Select a model card, enter your Google API key, and optionally customize the internal identifier

Available xAI Models:

Model Description Context Window Pricing
grok-4-latest Grok 4 - Latest flagship model 128K tokens $5/$15 per M tokens
grok-3-latest Grok 3 - Previous generation 128K tokens $3/$10 per M tokens
grok-3-mini-latest Grok 3 Mini - Compact and fast 128K tokens $1/$3 per M tokens
Setup: Select a model card, enter your xAI API key (xai-...), and optionally customize the internal identifier

Available Ollama Models (Local/Self-Hosted):

Model Description Context Window Size
llama3.2:1b Llama 3.2 1B - Ultra-lightweight 128K tokens 1.3 GB
llama3.2:3b Llama 3.2 3B - Balanced local model 128K tokens 2 GB
qwen2.5:7b Qwen 2.5 7B - Multilingual 32K tokens 4.7 GB
deepseek-coder-v2:16b DeepSeek Coder v2 - Code specialist 16K tokens 8.9 GB
mistral:7b Mistral 7B - General-purpose 32K tokens 4.1 GB
mixtral:8x7b Mixtral 8x7B - Mixture of experts 32K tokens 26 GB
Setup: Select a model card, set API URL to http://localhost:11434 (or your Ollama server), leave API key empty for local setup Note: Ollama models run locally and require Ollama to be installed and running on your machine or server

After selecting your model:

  1. Enter your API key (or configure API URL for Ollama)
  2. Set the internal_identifier - this is what you'll use in configuration references like embedding_model_name
  3. Click "Create LLM Model" to save
The model will be available immediately throughout the DSiloed platform with consistent pricing, context windows, and capabilities from the selected LlmModelProductType.
Connecting Claude Code to DSiloed MCP Server mdi-link

Connect Claude Code to your DSiloed tenant using the Model Context Protocol (MCP) to access all your data, conversations, memories, and platform capabilities directly from your development environment.

What is Claude Code?

Claude Code is Anthropic's official CLI for Claude that integrates AI assistance directly into your terminal and development workflow. By connecting it to DSiloed's MCP server, Claude gains access to your tenant's data and can help you manage projects, conversations, documents, and more.

Navigate to your DSiloed dashboard to copy your authentication token:

  1. Open dashboard.html in your browser
  2. Log in with your credentials if you haven't already
  3. Look for the JWT token display section
  4. Click the copy button to copy your JWT token to clipboard
Security Note: Your JWT token provides full access to your tenant data. Keep it secure and never share it publicly or commit it to version control.

Add the DSiloed MCP server configuration to your Claude Code settings:

Important: Configuration Location

The MCP server configuration is placed in the projects section of your ~/.claude.json file, where each project is keyed by its full directory path. Each project can have its own mcpServers configuration.

  1. Open or create your Claude Code configuration file at ~/.claude.json
  2. Locate or create the projects section
  3. Add an entry for your project using the full directory path as the key
  4. Inside that project, add the mcpServers object with your DSiloed configuration

Complete ~/.claude.json file structure:

{
  "projects": {
    "/home/username/my-project": {
      "mcpServers": {
        "harmoniq": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://dev.dsiloed.com/api/v1/mcp",
            "--header",
            "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
          ]
        }
      }
    }
  }
}
Configuration Notes:
  • Replace /home/username/my-project with the full path to your project directory
  • Replace YOUR_JWT_TOKEN_HERE with the JWT token you copied from the dashboard
  • For production: Use https://www.dsiloed.com/api/v1/mcp
  • For development: Use https://dev.dsiloed.com/api/v1/mcp
  • You can customize the server name (currently "harmoniq") to anything you prefer
  • On Windows, use forward slashes: C:/Users/username/my-project

Production example with actual token (truncated):

{
  "projects": {
    "/Users/john/projects/my-app": {
      "mcpServers": {
        "harmoniq": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://www.dsiloed.com/api/v1/mcp",
            "--header",
            "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ..."
          ]
        }
      }
    }
  }
}

Multiple projects with different configurations:

{
  "projects": {
    "/home/username/project-one": {
      "mcpServers": {
        "harmoniq": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://dev.dsiloed.com/api/v1/mcp",
            "--header",
            "Authorization: Bearer DEV_TOKEN_HERE"
          ]
        }
      }
    },
    "/home/username/project-two": {
      "mcpServers": {
        "harmoniq": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://www.dsiloed.com/api/v1/mcp",
            "--header",
            "Authorization: Bearer PROD_TOKEN_HERE"
          ]
        },
        "other-server": {
          "command": "some-command",
          "args": ["arg1"]
        }
      }
    }
  }
}
Pro Tip: Each project directory can have different MCP server configurations, allowing you to connect to different DSiloed tenants or environments based on the project you're working on.

Test your MCP connection:

  1. Start a new Claude Code session in your terminal
  2. The MCP server should connect automatically when Claude Code starts
  3. Ask Claude to list your conversations or check your memories to verify access

Example queries to test:

  • "Can you show me my recent conversations?"
  • "What memories do we have stored?"
  • "List my current tasks"
  • "Show me the parties in the system"
You're Connected!

Claude Code now has full access to your DSiloed tenant through 30+ MCP tools including data management, conversations, memories, agents, file operations, and more.

With Claude Code connected to DSiloed MCP, you can:

Data Management
  • Query and manage parties, contacts, tasks
  • Create and update business transactions
  • Access full CRUD operations
Conversations & Memory
  • Access conversation history
  • Load and store persistent memories
  • Maintain context across sessions
Agent Management
  • Create and configure AI agents
  • Execute agent runs
  • Monitor agent status
File Operations
  • Create and update documents
  • Read file contents
  • Manage conversation attachments

To reduce context size and focus on specific capabilities, you can filter which MCP tools are available by adding query parameters to your MCP endpoint URL.

Why Filter Tools?

With 39+ available tools, loading all of them adds significant context. Filtering allows you to load only the tools relevant to your current task, improving response times and reducing token usage.

Available Parameters:

  • categories - Comma-separated list of tool categories to include
  • tools - Comma-separated list of specific tool names to include

Available Categories:

Category Description Tool Count
agentsAgent management (create, execute, monitor)12
communicationEmail and invitation tools3
dataCRUD, schema, files, status7
memoryStore and load persistent memories2
conversationChat functionality3
app_generationBuilding applications5
dynamic_functionsServerless JS functions5
llm_toolsCustom tool management4
documentationDocumentation assistants2
adminAdministrative tools2
utilityGeneral utilities1
mdmMaster Data Management1

Example configurations:

Load only data and memory tools:

"https://dev.dsiloed.com/api/v1/mcp?categories=data,memory"

Load only agent management tools:

"https://dev.dsiloed.com/api/v1/mcp?categories=agents"

Load specific tools by name:

"https://dev.dsiloed.com/api/v1/mcp?tools=general_crud_tool,store_memory_tool,load_memories_tool"

Combine categories and specific tools:

"https://dev.dsiloed.com/api/v1/mcp?categories=data&tools=store_memory_tool"

Full ~/.claude.json example with filtering:

{
  "projects": {
    "/home/username/my-project": {
      "mcpServers": {
        "harmoniq": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://dev.dsiloed.com/api/v1/mcp?categories=data,memory,agents",
            "--header",
            "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
          ]
        }
      }
    }
  }
}
Note: If neither categories nor tools parameters are provided, all tools are returned (default behavior).
Step 3: Test Your AI Setup mdi-link

After configuring your LLM model, you can test it in several ways:

Chat Assistant

Look for the chat icon in any application's interface to start a conversation with your AI model.

LLM Manager

Use the conversation features in LLM Manager to test model responses directly.

Congratulations! Your AI is now set up and ready to use. The AI can help you with data analysis, content creation, and building applications throughout the DSiloed platform.
Available AI Features

With your LLM model configured, you can now use AI features throughout the platform:

Conversational AI

Chat with AI assistants that understand your business context and data.

App Generation

Get help building custom applications with AI-generated code and guidance.

Document Analysis

Upload and interact with documents using the Document Manager application.

Setting up an AI Agent

AI Agents are sophisticated autonomous systems that can perform complex tasks using AI models, tools, and scheduling. The Visual Agent Builder provides an intuitive interface for creating and configuring these powerful AI agents.

Understanding Agent Components

An AI Agent consists of eight key components, each serving a specific purpose in the agent's operation:

LLM Model

Purpose: The AI brain of your agent

Connects to your configured AI model (like Claude) to process information and make decisions. This determines the intelligence and capabilities of your agent.

Prompts (System + User)

Purpose: Dual-layer instructions for enhanced AI control

System Prompt: Defines personality, behavior patterns, and operational constraints

User Prompt: Contains specific task instructions and execution objectives

Roles

Purpose: Context-based behavioral guidelines

Assigns specific roles (like Software Engineer, Designer) that provide context and persona for execution. Agents can use multiple roles or specific role combinations during execution.

Resources

Purpose: Data sources and information access

Provides the agent with access to databases, APIs, documents, and other information sources it needs to complete its tasks effectively.

Schedule

Purpose: Automated execution timing

Controls when and how often the agent runs. Can be set for one-time execution, recurring schedules, or event-triggered activation.

Tools

Purpose: Actions the agent can perform

Equips the agent with specific capabilities like sending emails, creating reports, updating databases, or integrating with external systems.

Conversations

Purpose: Communication history and context

Maintains conversation history and context, allowing the agent to remember previous interactions and maintain continuity across sessions.

Security Roles

Purpose: Permission-based access control

Controls what capabilities and resources the agent can access. Separate from behavioral roles, these determine API permissions and security constraints.

Setting Up Your First Agent

Follow these steps to create your own AI Agent using the Visual Agent Builder:

Navigate to the LLM Manager application and look for the "Agent Builder" or "Visual Agent Builder" option. This opens the drag-and-drop interface shown in the screenshot above.

Set up the essential components in this order:

  • Agent Name: Give your agent a descriptive name (e.g., "Content Creation Agent")
  • LLM Model: Select your configured AI model (e.g., Claude 3.5 Sonnet)
  • System Prompt: Define the agent's personality, behavior patterns, and operational guidelines
  • User Prompt: Write specific task instructions for what the agent should accomplish
  • Roles (Pink Card): Assign behavioral roles like "Software Engineer" or "Designer" to provide execution context
  • Security Roles (Purple Card): Assign permission roles like "Content Manager" to control API and resource access

Equip your agent with the capabilities it needs:

  • Tools: Select from system tools (email, database) or create custom tools
  • Resources: Grant access to specific data sources or APIs
  • Connect Components: Use the visual interface to link components together

Configure when your agent should run:

  • Manual: Run on-demand when triggered
  • Scheduled: Set recurring intervals (every 5 minutes, daily, weekly)
  • Event-triggered: Activate based on specific system events

Before activating your agent:

  • Manual Execution: Use the Execute button to run the agent manually with optional role selection
  • Role Testing: Try different role combinations to test behavior variations
  • Additional Instructions: Test with custom execution instructions to ensure flexibility
  • Review Connections: Ensure all components are properly linked in the Visual Builder
  • Activate: Turn on the agent to begin automated scheduled operation

Manual Agent Execution with Role Selection

Beyond scheduled automation, agents can be executed manually with fine-grained control over their behavior through role selection and additional instructions.

Manual Execution

Click the Execute button on any agent to:

  • Run the agent immediately without waiting for schedule
  • Add specific instructions for this execution
  • Select which roles the agent should use
  • Test agent behavior before scheduling
Role Selection

During manual execution:

  • All agent roles are pre-selected by default
  • Deselect specific roles to modify behavior
  • Each role includes ID and persona details
  • Agents adapt behavior based on selected roles
Advanced Tip: Use manual execution with different role combinations to test how your agent behaves in various contexts. For example, a content agent with only "Technical Writer" role selected might focus more on documentation, while adding "Marketing Specialist" could make it more promotional. Enhanced Example: A "Content Creation Agent" with roles like "Software Engineer" and "Technical Writer" could automatically generate documentation. During manual execution, you could select only the "Technical Writer" role for user-focused docs, or both roles for technical implementation guides, while adding specific instructions like "focus on API endpoints."

Agent Architecture: Party and User Integration

Each LLM Agent is automatically configured with both Party and User entities, providing comprehensive identity and security management:

Party Entity (Identity)
  • Provides agent identity in the party management system
  • Enables role-based behavior through PartyRoles
  • Links agent to broader organizational structure
  • Supports behavioral context for execution
User Account (Security)
  • Username matches the agent's name
  • Provides security context through SecurityRoles
  • Enables capability-based access control
  • No email required (agent users are email-less)

Dual Role System in Visual Agent Builder

The Visual Agent Builder includes two distinct role management systems, each serving different purposes:

Roles Card (Behavioral)

Purpose: Define agent personality and expertise

  • Pink-themed visual card positioned above agent
  • Shows current PartyRole count
  • Click to manage behavioral roles
  • Controls execution context and decision making
  • Examples: "Software Engineer", "Technical Writer"
Security Roles Card (Permissions)

Purpose: Control API and resource access

  • Purple-themed visual card positioned below agent
  • Shows current SecurityRole count
  • Click to manage permission roles
  • Defines what capabilities the agent can access
  • Examples: "Content Manager", "Data Analyst"

Security Role Management

Security roles are assigned to the agent's User account for fine-grained permission control. Here's how to manage them programmatically:

API Example: Assign Security Role
// Assign a security role to an agent's user account
await apiClient.createSecurityRoleAssignment({
  accessor_record_type: 'User',
  accessor_record_id: agent.user.id,
  security_role_id: securityRoleId
});

// Example: Assign content manager role
const contentManagerRole = await apiClient.getSecurityRoles({ 
  internal_identifier: 'content_manager' 
});

await apiClient.createSecurityRoleAssignment({
  accessor_record_type: 'User',
  accessor_record_id: agent.user.id,
  security_role_id: contentManagerRole.security_roles[0].id
});
API Response Structure: LLM Agent responses include comprehensive data about both Party and User relationships. The user object contains security_roles array, while party_roles contains behavioral roles with full role_type details. Security Best Practices:
  • Use Principle of Least Privilege - only assign security roles agents actually need
  • Separate behavioral roles (PartyRoles) from permission roles (SecurityRoles)
  • Regularly audit agent security role assignments
  • Always review and test agents thoroughly before activation

LLM Memory System

mdi-link
What is LLM Memory?

The LLM Memory System provides persistent memory capabilities for AI agents and conversations, enabling long-term learning, pattern recognition, and contextual awareness across sessions. Unlike simple chat history, the memory system uses advanced vector embeddings and graph-based relationships to store and retrieve information semantically.

Why is Memory Important? With LLM Memory, your AI can remember user preferences, previous conversations, learned patterns, and contextual information across sessions. This dramatically improves the quality and relevance of AI responses, making interactions feel more natural and personalized.
Key Features Vector Embeddings

85-100% Accuracy: Advanced semantic similarity matching using OpenAI embeddings

Find memories based on meaning, not just keywords. "What does Ben like?" matches "Ben Koloski loves spaghetti"

Graph-Based Storage

Relationship Tracking: Memories are connected through typed relationships

Build knowledge graphs showing how facts, preferences, and insights relate to each other

Automatic Pattern Recognition

Background Processing: Daily consolidation jobs identify patterns

Automatically extract insights from related memories and create derived knowledge

Multi-Tenant Isolation

Secure by Design: Complete data isolation between tenants

Each tenant's memories are stored separately with capability-based access control

Memory Types

Fact: Specific facts, data points, or verified information Preference: User preferences, settings, or choices Decision: Important decisions made during conversations Insight: Analysis, conclusions, or discovered patterns Context: Background information or situational context Solution: Technical solutions, fixes, or configurations
Configuring LLM Memory

LLM Memory is configured through the memory_management Configuration. The system requires OpenAI's Text Embedding 3 Small model for vector embeddings, which provides high-quality semantic similarity matching.

⚠️ OpenAI Text Embedding 3 Small REQUIRED:
  1. Only OpenAI Supported: The database uses 1536-dimensional vectors matching OpenAI Text Embedding 3 Small. Other providers are incompatible.
  2. Create LlmModel First: In LLM Manager, create an LlmModel using "Text Embedding 3 Small (1536-dim)" with your OpenAI API key.
  3. Use internal_identifier: Reference your embedding model by its internal_identifier in the configuration.

Configuration Parameters

Parameter Description Example Value
enable_embeddings Enable vector embedding generation for semantic search true
embedding_model_name internal_identifier of your OpenAI embedding LlmModel 'openai-embeddings'
embedding_dimensions FIXED: Must be 1536 (OpenAI Text Embedding 3 Small) 1536
embedding_similarity_threshold Minimum similarity score (0.0-1.0) for memory retrieval 0.7
max_embedding_length Maximum characters to process for embeddings 8000
classification_model_name internal_identifier of LlmModel for semantic relationship classification (optional) 'classifier' or null
classification_method Classification approach: 'heuristic', 'llm', or 'hybrid' (recommended) 'hybrid'
classification_llm_threshold_score Minimum similarity score (0.0-1.0) to trigger LLM classification 0.9
classification_llm_threshold_importance Minimum importance score (0.0-1.0) for LLM classification 0.8
classification_max_llm_calls_per_consolidation Maximum LLM API calls per consolidation run (cost control) 50
Semantic Relationship Classification: The classification parameters control how the system determines the type of relationship between memories (e.g., "caused_by", "leads_to", "supersedes"). Setting classification_model_name enables LLM-based classification for highly accurate semantic relationships. If null, the system uses fast rule-based heuristics only.

Configuration Example

Using the API
// Step 1: Create OpenAI embedding model in LLM Manager first
// Step 2: Optionally create a classifier model (e.g., GPT-3.5-turbo or GPT-4)
// Step 3: Use internal_identifiers in this configuration

const response = await fetch('/api/v1/configurations', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + token,
    'Tenant-Id': tenantId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    configuration: {
      internal_identifier: 'memory_management',
      configuration_type_id: 'memory_management',
      configuration_values: {
        // Vector Embeddings (Required for semantic search)
        enable_embeddings: true,
        embedding_model_name: 'openai-embeddings',  // Your embedding LlmModel
        embedding_dimensions: 1536,                  // Fixed - do not change
        embedding_similarity_threshold: 0.7,
        max_embedding_length: 8000,

        // Relationship Classification (Optional but recommended)
        classification_model_name: 'classifier',     // Your classifier LlmModel (null = heuristics only)
        classification_method: 'hybrid',             // 'heuristic', 'llm', or 'hybrid'
        classification_llm_threshold_score: 0.9,     // Similarity threshold for LLM usage
        classification_llm_threshold_importance: 0.8, // Importance threshold for LLM usage
        classification_max_llm_calls_per_consolidation: 50  // Cost control limit
      }
    }
  })
});
Using Rails Console
# Step 1: Create OpenAI embedding model in LLM Manager first
# Step 2: Optionally create a classifier model (e.g., GPT-3.5-turbo or GPT-4)
# Step 3: Use internal_identifiers in this configuration

# Find your tenant
tenant = Tenant.find_by(enterprise_identifier: 'your-tenant')

# Create or update memory_management configuration
config = Configuration.find_or_initialize_by(
  tenant: tenant,
  internal_identifier: 'memory_management'
)

config.update!(
  configuration_values: {
    # Vector Embeddings (Required for semantic search)
    'enable_embeddings' => true,
    'embedding_model_name' => 'openai-embeddings',  # Your embedding LlmModel
    'embedding_dimensions' => 1536,                  # Fixed - do not change
    'embedding_similarity_threshold' => 0.7,
    'max_embedding_length' => 8000,

    # Relationship Classification (Optional but recommended)
    'classification_model_name' => 'classifier',     # Your classifier LlmModel (nil = heuristics only)
    'classification_method' => 'hybrid',             # 'heuristic', 'llm', or 'hybrid'
    'classification_llm_threshold_score' => 0.9,     # Similarity threshold for LLM usage
    'classification_llm_threshold_importance' => 0.8, # Importance threshold for LLM usage
    'classification_max_llm_calls_per_consolidation' => 50  # Cost control limit
  }
)

Semantic Relationship Classification

When memories are connected, the system automatically classifies the type of relationship between them using intelligent semantic analysis. This creates meaningful knowledge graphs instead of generic "related to" connections.

Rule-Based Classification

Fast, zero-cost pattern matching

Uses keyword detection to identify relationships like "caused_by", "leads_to", "supersedes"

Example: "Fixed bug caused by null pointer" → detects "caused_by" relationship

LLM-Based Classification

High-accuracy semantic analysis

Uses configured LLM model for precise relationship classification on important memories

Triggered selectively based on similarity and importance thresholds to manage costs

Hybrid Approach (Recommended): Combines fast rule-based heuristics with selective LLM classification for important cases. This provides excellent accuracy while keeping costs low (typically <5% of relationships use LLM, ~$0.01/day for active tenants).

Available Relationship Types

caused_by: Causal relationships (bug caused by configuration) leads_to: Consequence relationships (decision leads to implementation) contradicts: Conflicting information references: Cross-references (implementation references design) spawns: Parent-child generation (feature spawns subtasks) supersedes: Replacement (new approach supersedes old solution) validates: Test or verification exemplifies: Pattern demonstration

Automatic Background Processing

Once configured, the memory system runs automatic background jobs to maintain and enhance memory quality:

Memory Consolidation (Daily 3am): Identifies patterns, creates relationships, extracts derived insights Memory Pruning (Weekly Sunday 4am): Cleans up low-importance memories and consolidates duplicates Agent Learning (Daily 6am): Analyzes execution patterns and improves agent performance
Using Memory in Your Applications

The memory system integrates seamlessly with AI conversations and agents. Memories are automatically loaded and used to provide context for AI responses.

Common Use Cases

Personalized AI Assistants

Remember user preferences, work patterns, and communication styles across sessions for natural, personalized interactions

Development Agents

Store solutions, code patterns, and architectural decisions to improve code quality and consistency over time

Healthcare Applications

Remember patient history, treatment preferences, and clinical insights while maintaining HIPAA compliance through multi-tenant isolation

Business Intelligence

Track customer insights, market trends, and strategic decisions to build organizational knowledge over time

Recommended: Configure LLM Memory to unlock the full potential of your AI applications. The semantic search capabilities and automatic pattern recognition significantly improve AI response quality and user satisfaction.

Chat Features and LLM Integration

mdi-link
ChatAssistant Component

DSiloed includes a powerful, reusable ChatAssistant component that provides AI-powered chat capabilities to any application. The component is framework-independent and features a modern, Discord-like interface with conversation management.

Key Features

Responsive design with quarter-screen flyout that expands to fullscreen Real-time chat with AI assistants using @ai mentions Multi-member conversations with role-based access File attachment support (PDFs, images, documents) Dark mode support with automatic theme detection Webhook integration for external message ingestion
@AI Mentions and LLM Integration

The ChatAssistant component supports intelligent @ai mentions that allow users to interact with specific LLM models within conversations.

@AI Mention Syntax

@ai - Uses the default AI model @ai:model_name - Uses a specific AI model @ai:model_name/compact - Uses a specific model with compaction @ai/compact - Uses default model with conversation compaction

Examples

@ai Can you help me analyze this data? Uses the default AI model @ai:claude-3-5-sonnet Please review this code Uses Claude 3.5 Sonnet specifically @ai:gpt-4/compact Summarize our conversation Uses GPT-4 with conversation compaction Note: Messages without @ai mentions are saved as user messages without triggering AI responses, allowing for mixed human-AI conversations.
Implementation Guide

Basic Setup

// Include the ChatAssistant component
<script src="js/components/ChatAssistant.js?v=1771863622"></script>

// Initialize with your API client
const chatAssistant = new ChatAssistant('YourAppName', {
    apiClient: apiClient,
    title: 'AI Assistant',
    placeholder: 'Ask me anything...',
    contextMessage: 'Assistant has access to your data'
});

// Toggle chat visibility
chatAssistant.toggle();

Vue.js Integration

// In your Vue component
methods: {
    initChatAssistant() {
        this.chatAssistant = new ChatAssistant('MyApp', {
            apiClient: this.apiClient,
            title: 'MyApp Assistant',
            onError: (error) => {
                this.showSnackbar('Chat error: ' + error.message, 'error');
            }
        });
    },
    toggleChat() {
        if (this.chatAssistant) {
            this.chatAssistant.toggle();
        }
    }
},
mounted() {
    this.initChatAssistant();
}

React Integration

// In your React component
import { useEffect, useRef } from 'react';

function MyApp() {
    const chatAssistantRef = useRef(null);
    
    useEffect(() => {
        chatAssistantRef.current = new ChatAssistant('MyApp', {
            apiClient: apiClient,
            title: 'MyApp Assistant'
        });
    }, []);
    
    const toggleChat = () => {
        if (chatAssistantRef.current) {
            chatAssistantRef.current.toggle();
        }
    };
    
    return (
        <button onClick={toggleChat}>Toggle Chat</button>
    );
}
Webhooks for External Integration

Webhooks allow external systems to send messages directly into conversations without authentication. This enables integration with external applications, chatbots, monitoring systems, and more.

Creating Webhooks

Webhooks can be created through the ChatAssistant Settings interface or via API. Each webhook generates a unique URL that external systems can use to post messages.

Webhook API Endpoint

POST /api/v1/webhooks/:key/publish_message

Example CURL Request

# Send a message via webhook
curl -X POST "https://your-domain.com/api/v1/webhooks/your-webhook-key/publish_message" \
     -H "Content-Type: application/json" \
     -d '{
       "message": "Alert: Server CPU usage is at 85%"
     }'

Advanced Usage with @AI Mentions

# Send a message that triggers AI response
curl -X POST "https://your-domain.com/api/v1/webhooks/your-webhook-key/publish_message" \
     -H "Content-Type: application/json" \
     -d '{
       "message": "@ai Please analyze this error and suggest a solution: Database connection timeout after 30 seconds"
     }'

Creating Webhooks via API

// Create a new webhook for a conversation
const webhookData = {
  webhook: {
    name: "Server Monitoring",
    description: "Webhook for server monitoring alerts",
    related_entity_type: "LlmConversation",
    related_entity_id: conversationId,
    direction: "inbound"
  }
};

const response = await fetch('/api/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + token,
    'Tenant-Id': tenantId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(webhookData)
});

const result = await response.json();
console.log('Webhook URL:', result.webhook.url);
Security Note: Webhook URLs contain unique keys and should be treated as sensitive. Store them securely and rotate them if compromised.
Configuration Options

ChatAssistant Configuration

apiClient (required) API client instance for making requests title Chat window title (default: "AI Assistant") placeholder Input placeholder text contextMessage Context message shown at top of chat position 'left' or 'right' (default: 'right') onToggle Callback function when chat is toggled onMessage Callback function for new messages onError Error handler callback function

MCP Tools Reference

mdi-link
About MCP Tools

Model Context Protocol (MCP) tools provide a standardized interface for AI models to interact with your DSiloed platform. These tools enable LLMs to perform operations, access data, manage agents, handle communications, and more.

All MCP tools are available through the MCP server and can be used by external applications, AI agents, and LLM conversations. This reference lists all 35+ available MCP tools with their descriptions and use cases.

Note: These tools are automatically available to LLM agents configured in your tenant. External applications can also access these tools through the MCP protocol.
mdi-folder-multiple Tool Categories mdi-link

Tools are organized into categories for easier filtering and discovery. Use these category names with the categories query parameter to load specific tool groups.

Category Description Tools
agents Agent management - create, configure, execute, and monitor AI agents 12 tools
communication Email sending and conversation invitations 3 tools
data CRUD operations, schema discovery, file management, status tracking 7 tools
memory Persistent memory storage and retrieval for AI context 2 tools
conversation Chat functionality and conversation management 3 tools
app_generation LLM-powered web application building 5 tools
dynamic_functions Serverless JavaScript function management 5 tools
llm_tools Custom LLM tool creation and management 4 tools
documentation Documentation assistants for platform and app development 2 tools
admin Administrative tools (logs, cost statistics) 2 tools
utility General utilities (date/time) 1 tool
mdm Master Data Management for external system mappings 1 tool
Filtering Tools: Add ?categories=data,memory to your MCP endpoint URL to load only specific categories. See the Claude Code setup guide for detailed filtering instructions.
mdi-email Communication Tools

Tools for sending emails and managing conversation invitations.

send_email_tool

Send emails with customizable subject, body, recipients, and sender information.

Use Cases:

  • Send notifications to users
  • Email reports and summaries
  • Bulk communications to multiple recipients

Key Parameters:

  • subject - Email subject line
  • email_body - HTML email content
  • to_email - Recipient(s), semicolon-separated
  • from_email - Optional sender address
send_conversation_invitation_tool

Send single-use invitation links for one-time feedback in LLM conversations.

Use Cases:

  • Request specific feedback or answers
  • One-time input collection
  • Quick survey responses
send_guest_invitation_tool

Send guest trial invitations allowing multiple messages before signup.

Use Cases:

  • Progressive user engagement
  • Trial conversation access
  • Guest-to-member conversion flows
mdi-database Data Management Tools

Tools for CRUD operations, schema discovery, and file management.

general_crud_tool Most Used

Comprehensive CRUD operations for all data models with advanced filtering, relationships, and aggregations.

Capabilities:

  • Create, Read, Update, Delete operations
  • Advanced search with JSON query syntax
  • Relationship management (link/unlink)
  • Aggregations and analytics
  • Field-level selection for performance

Supported Models:

Party, Individual, Organization, Contact, BizTxnEvent, Project, Task, LlmConversation, and 30+ more

model_schema_tool

Discover schema information, available columns, required fields, and data types for any model.

Use Cases:

  • Understand model structure before queries
  • Identify required fields for creation
  • Discover available relationships
create_llm_file_tool

Create text-based documents (Markdown, TXT, CSV, PDF, DOCX) programmatically.

Use Cases:

  • Generate reports and summaries
  • Create documentation
  • Save analysis results
update_llm_file_tool

Update existing documents with new content, titles, or move to different directories.

read_llm_file_tool

Read file contents including text files, documents (PDF/DOCX), images, and media files.

llm_cost_stats_tool

Retrieve comprehensive LLM usage statistics, costs, and token consumption by model, user, and conversation.

apply_status_tool

Apply tracked statuses to entities (Tasks, Projects, BizTxnEvents, etc.) with proper history tracking.

mdi-robot Agent Management Tools

Tools for creating, managing, and executing autonomous AI agents.

create_agent_tool

Create complete LLM agents with custom tools, resources, prompts, and role assignments.

update_agent_tool

Modify existing agent configurations, prompts, tools, and settings.

delete_agent_tool

Permanently remove agents from the system.

list_available_agents_tool

List all available agents with their capabilities and descriptions.

execute_agent_tool

Execute an agent with specific prompts and monitor execution status.

check_agent_status_tool

Check the execution status and output of running agents.

complete_agent_run_tool

Mark agent runs as complete with success/failure status.

list_available_agent_tools_tool

List all tools that can be assigned to agents during creation.

list_available_agent_resources_tool

List all resources available for agent configuration.

list_available_llm_models_tool

List configured LLM models available for agent creation.

list_available_prompts_tool

List available system and user prompts for agents.

get_tool_configuration_tool

Get configuration requirements for specific tools before assigning to agents.

mdi-brain Memory Tools

Tools for persistent memory management across AI sessions.

store_memory_tool

Store facts, insights, decisions, preferences, and context as persistent memories.

Memory Types:

  • Fact - Specific data points and verified information
  • Preference - User settings and choices
  • Decision - Important choices made
  • Insight - Analysis and patterns
  • Solution - Technical fixes and configurations
  • Goal - User objectives
load_memories_tool

Retrieve relevant memories to provide context for AI conversations.

Retrieval Modes:

  • Search - Content-based keyword search
  • Semantic - Vector similarity search
  • Recent - Recently created memories
  • Important - High-importance memories
  • Conversation/Agent specific
mdi-chat Conversation Tools

Tools for managing LLM conversations and messaging.

conversation_chat_tool

Send messages to LLM conversations and trigger AI responses.

Use Cases:

  • External system integration
  • Automated message sending
  • Custom chat interfaces
mdi-book-open Documentation Tools

Tools for accessing platform documentation.

dmapi_assistant_tool

Ask questions about the Data Model API platform implementation and features.

Topics Covered:

  • Security and capability system
  • API endpoints and responses
  • Model relationships
  • MCP integration
  • Webhook system
app_docs_assistant_tool

Access app development documentation for building applications on the platform.

Topics Covered:

  • Authentication setup
  • API client usage
  • Shared components
  • Best practices
mdi-tools Utility & Admin Tools

Utility and administrative tools for system operations.

current_date_time_tool

Get current date and time with timezone support and multiple formats.

tail_rails_log_tool Admin Only

Root tenant only: Tail Rails application logs for debugging and monitoring.

external_mappings_by_system_tool

Get external system mappings for MDM integration and data synchronization.

mdi-function Dynamic Functions Tools

Tools for creating, managing, and executing serverless JavaScript functions.

list_dynamic_functions_tool

List available dynamic functions with their status, schedule, and recent execution info.

Filtering Options:

  • active_only - Only show active functions
  • scheduled_only - Only show scheduled functions
  • callbacks_only - Only show callback functions
  • callback_model - Filter by callback model name
  • name - Filter by function name (partial match)
get_dynamic_function_tool

Get full details of a specific function by ID or name, including JavaScript code and execution history.

Key Parameters:

  • id or name - Function identifier
  • include_executions - Include recent execution history
  • execution_limit - Number of executions to include (max 20)
execute_dynamic_function_tool

Execute a dynamic function by ID or name and return results.

Key Parameters:

  • function_id or function_name - Function to execute
  • params - Parameters passed to the function
  • async - Execute asynchronously (queued)

Returns:

  • success - Execution status
  • result - Function return value
  • execution_id - For tracking history
  • execution_time_ms - Duration
manage_dynamic_function_tool CRUD

Create, update, or delete dynamic functions with full configuration support.

Actions:

  • create - Create new function with name, code, schedule, callbacks
  • update - Modify existing function (can rename with new_name)
  • delete - Remove function

Configuration Options:

  • schedule - Cron expression for scheduled execution
  • callback_model - Model to trigger callback on
  • callback_event - Event type (create, update, destroy, all)
  • timeout_seconds - Max execution time (1-300)
  • configuration - Custom config including OAuth
dynamic_function_executions_tool

View execution history and logs for debugging and monitoring.

Filtering Options:

  • function_id or function_name - Filter by function
  • execution_id - Get specific execution details
  • trigger_type - Filter by trigger (manual, api, schedule, mcp_tool, webhook, nested_call, callback)
  • success_only / failed_only - Filter by status
  • limit - Number of records (max 100)
Tip: Dynamic Functions can access tenant data via executeAction(), make HTTP requests with fetch(), call other functions with executeFunction(), and integrate with external systems via OAuth. See the Dynamic Functions section for full documentation.
Using MCP Tools

MCP tools can be accessed in multiple ways:

In LLM Agents

Configure tools when creating agents in LLM Manager. Agents can use these tools autonomously during execution.

In Conversations

LLMs in conversations can invoke tools as needed to answer questions, retrieve data, or perform actions.

External Applications

Connect external applications via MCP protocol to access all tools programmatically.

Custom Tools

Create custom tools in LLM Manager using JavaScript for tenant-specific functionality.

Pro Tip: Combine multiple tools in agent workflows for complex automation. For example, use general_crud_tool to fetch data, create_llm_file_tool to generate a report, and send_email_tool to distribute it.

API Documentation

mdi-link
RESTful API Overview

DSiloed provides a comprehensive RESTful API for building applications. All endpoints follow consistent RESTful conventions with JSON responses.

Authentication

Authentication uses JWT (JSON Web Tokens) passed in the Authorization header.

curl -X GET "https://example-model-api.com/api/v1/parties" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

Content Types

All API requests should use JSON for request bodies and accept JSON responses.

-H "Content-Type: application/json" \
-H "Accept: application/json"

Response Format

Responses follow a consistent format with data returned in an object named after the resource.

// Single resource example (e.g., GET /api/v1/users/123)
{
  "success": true,
  "user": {
    "id": 123,
    "description": "John Doe",
    "created_at": "2025-05-01T12:34:56Z",
    "updated_at": "2025-05-02T10:11:12Z"
  }
}

// Multiple resources example (e.g., GET /api/v1/users)
{
  "success": true,
  "total_count": 42,
  "users": [
    {
      "id": 123,
      "description": "John Doe",
      "created_at": "2025-05-01T12:34:56Z"
    },
    {
      "id": 124,
      "description": "Jane Smith",
      "created_at": "2025-05-02T08:15:30Z"
    }
    // ... more users
  ]
}

Error Handling

Errors return a simple format with success set to false and an error message.

{
  "success": false,
  "message": "Invalid Access"
}
Core Resources Parties Contacts Business Events Security LLM Integration

Parties API

Parties represent individuals and organizations. The Party model is the foundation of your application's data model.

List all parties

curl -X GET "https://example-model-api.com/api/v1/parties" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

Get a specific party

curl -X GET "https://example-model-api.com/api/v1/parties/123" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

Create an individual

curl -X POST "https://example-model-api.com/api/v1/individuals" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id" \
    -H "Content-Type: application/json" \
    -d '{
      "first_name": "John",
      "last_name": "Doe",
      "dob": "1990-01-01"
    }'

Create an organization

curl -X POST "https://example-model-api.com/api/v1/organizations" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id" \
    -H "Content-Type: application/json" \
    -d '{
      "description": "Acme Corporation"
    }'

Contacts API

Contacts represent various contact methods like email addresses, phone numbers, and postal addresses.

List all contacts

curl -X GET "https://example-model-api.com/api/v1/contacts" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

Create an email address

curl -X POST "https://example-model-api.com/api/v1/email_addresses" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id" \
    -H "Content-Type: application/json" \
    -d '{
      "party_id": 123,
      "description": "Work Email",
      "email_address": "john.doe@example.com"
    }'

Create a phone number

curl -X POST "https://example-model-api.com/api/v1/phone_numbers" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id" \
    -H "Content-Type: application/json" \
    -d '{
      "party_id": 123,
      "description": "Mobile",
      "phone_number": "+15551234567"
    }'

Business Events API

Business events track activities and transactions within your application.

List all business events

curl -X GET "https://example-model-api.com/api/v1/biz_txn_events" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

Create a business event

curl -X POST "https://example-model-api.com/api/v1/biz_txn_events" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id" \
    -H "Content-Type: application/json" \
    -d '{
      "biz_txn_event_type_id": 1,
      "description": "Order Placed",
      "event_date": "2025-05-11T14:30:00Z",
      "amount": 99.99,
      "payload": {
        "order_number": "ORD-12345",
        "items": 3
      }
    }'

Security API

Manage security, roles, and capabilities within your application.

List all security roles

curl -X GET "https://example-model-api.com/api/v1/security_roles" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

List all capabilities

curl -X GET "https://example-model-api.com/api/v1/capabilities" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

LLM Integration API

APIs for managing LLM models, tools, and conversations.

List all LLM models

curl -X GET "https://example-model-api.com/api/v1/llm_models" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"

List all LLM tools

curl -X GET "https://example-model-api.com/api/v1/llm_tools" \
    -H "Authorization: Bearer YOUR_JWT_TOKEN" \
    -H "Tenant-Id: your-tenant-id"
Swagger Documentation

For complete API documentation, refer to the Swagger UI which provides interactive documentation for all available endpoints.

Open Swagger Documentation

Guest User Invitations

mdi-link
Overview

Guest Users allow you to invite new users who have never used the platform before. These "guest users" are created directly in your tenant with scoped access to specific resources, making them ideal for contractors, clients, and consultants who need temporary or limited access.

Guest Users vs Cross-Tenant Sharing

Factor Guest User Cross-Tenant Sharing
User Status New to platform Existing tenant user
Account Creation Create user in your tenant User already exists
User Experience Single workspace (your tenant) Multiple workspaces (switch between)
Best For Contractors, clients, consultants Partners, service providers
Conversion Path Can upgrade to full tenant later Already has full tenant

Guest User Workflow

Create a new user in your tenant with the is_guest: true flag.

Basic Guest User Creation

// Create guest user account with auto-generated password
const createGuestUser = async (username, email) => {
  const response = await fetch('/api/v1/users', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user: {
        first_name: 'John',
        last_name: 'Contractor',
        username: username,
        email: email,
        // password is OPTIONAL for guest users - auto-generated if not provided
        is_guest: true,
        custom_fields: {
          invited_by: 'Your Name',
          purpose: 'Project collaboration'
        }
      },
      guest_user_security_roles: 'basic_user,project_viewer'  // Comma-separated role IIDs
    })
  });

  const data = await response.json();
  // Returns: { success: true, user: { id: 456, username, party_id: 789, is_guest: true, has_temp_password: true, ... }}
  return data.user;
};

Advanced: Custom Password & Email Template

// Create guest user with custom password and email template
const createGuestUserCustom = async (username, email, customPassword) => {
  const customTemplate = `
    <h2>Welcome to Our Platform, {{firstName}}!</h2>
    <p>You've been invited to collaborate on a project.</p>

    <h3>Login Information:</h3>
    <ul>
      <li>Email: {{email}}</li>
      <li>Temporary Password: {{temp_password}}</li>
      <li>Login URL: {{login_link}}</li>
    </ul>

    <p>Please change your password after first login.</p>
  `;

  const response = await fetch('/api/v1/users', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user: {
        first_name: 'John',
        last_name: 'Contractor',
        username: username,
        email: email,
        password: customPassword,  // Optional: provide custom password
        is_guest: true
      },
      guest_user_security_roles: 'basic_user,project_viewer',
      guest_invite_email_template: customTemplate,
      guest_invite_email_subject: 'Welcome to Our Project!'
    })
  });

  const data = await response.json();
  return data.user;
};
What Happens Automatically:
  • Password Generation: If password is omitted, a secure 16-character password is automatically generated
  • Security Role Assignment: Roles specified in guest_user_security_roles are assigned immediately
  • Email Sent: Guest user receives an invitation email with credentials and login URL automatically
  • Password Change Flag: The has_temp_password flag is set, requiring password change on first login

That's it! No additional steps needed for basic guest user access. The user will receive the email and can log in immediately.

Note: Guest users are created directly in your tenant, not as separate tenants. They have full user accounts but are marked with is_guest: true and has_temp_password: true. The security roles you assign determine their level of access to your system.

The guest user receives an email with their credentials and follows this login workflow:

What the Guest User Experiences:

  1. Receives Email: Guest gets an email with their username/email, temporary password, and login URL
  2. First Login: Guest logs in with the temporary credentials
  3. Password Change Prompt: The login response includes requires_password_change: true
  4. Updates Password: Guest must change password before accessing the system

Login Response for Guest Users

// Guest user login
const response = await fetch('/api/v1/users/login', {
  method: 'POST',
  headers: {
    'Tenant-Id': currentTenantId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'john@example.com',
    password: 'temporary_password_from_email'
  })
});

const data = await response.json();
// Response: {
//   success: true,
//   token: 'jwt_token_here',
//   user: { id: 456, email: 'john@example.com', is_guest: true, ... },
//   requires_password_change: true  // Present for guest users with temp passwords
// }

// If requires_password_change is true, show password change form
if (data.requires_password_change) {
  showPasswordChangeDialog();
}

Password Change Request

// Update password after first login
const changePassword = async (newPassword) => {
  const response = await fetch('/api/v1/users/current/update_password', {
    method: 'PUT',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      password: newPassword
    })
  });

  const data = await response.json();
  // Response: { success: true }
  // The has_temp_password flag is automatically cleared
};
Automatic Cleanup: When the guest user changes their password, the has_temp_password flag is automatically cleared. Future logins will not require a password change.

Note: This step is optional. If the security roles assigned in Step 1 provide sufficient access, you don't need scoped capabilities. Use this approach when you need to restrict guest users to specific records (e.g., only one project or specific documents).

1. Create Scoped Capabilities

// Create project-scoped capability for guest user
const createProjectCapability = async (projectId) => {
  const response = await fetch('/api/v1/capabilities', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      capability: {
        action: 'view',
        resource: 'Project',
        accessor_record_specific: true,  // Auto-delete when assignment removed
        description: 'Guest access to specific project',
        scope: {
          where: { id: projectId },
          fields: ['id', 'name', 'description', 'status', 'created_at']
        }
      }
    })
  });

  const data = await response.json();
  return data.capability;
};

// Create task-scoped capability
const createTaskCapability = async (projectId) => {
  const response = await fetch('/api/v1/capabilities', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      capability: {
        action: 'view',
        resource: 'Task',
        accessor_record_specific: true,
        description: 'Guest access to project tasks',
        scope: {
          where: { project_id: projectId }
        }
      }
    })
  });

  const data = await response.json();
  return data.capability;
};

2. Assign Capabilities to Guest User

// Assign capability to guest user
const assignCapabilityToGuest = async (capabilityId, guestUserId) => {
  const response = await fetch('/api/v1/capability_assignments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      capability_assignment: {
        capability_id: capabilityId,
        accessor_record_type: 'User',
        accessor_record_id: guestUserId
      }
    })
  });

  const data = await response.json();
  return data.capability_assignment;
};

3. Complete Scoped Guest User Setup

// Complete workflow with scoped capabilities
const setupGuestUserWithScoping = async (projectId) => {
  // 1. Create guest user (automatically assigns roles and sends email)
  const response = await fetch('/api/v1/users', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user: {
        first_name: 'John',
        last_name: 'Contractor',
        username: 'contractor.john',
        email: 'john@example.com',
        is_guest: true
      },
      guest_user_security_roles: 'basic_user'  // Basic role first
    })
  });

  const guestData = await response.json();
  const guest = guestData.user;

  // 2. Create scoped capabilities for specific project access
  const projectCap = await createProjectCapability(projectId);
  const taskCap = await createTaskCapability(projectId);

  // 3. Assign scoped capabilities
  await assignCapabilityToGuest(projectCap.id, guest.id);
  await assignCapabilityToGuest(taskCap.id, guest.id);

  // Guest now has:
  // - Basic role permissions (from security role)
  // - Plus scoped access to specific project and its tasks (from capabilities)

  return guest;
};
accessor_record_specific: Set to true to automatically delete the capability when all assignments are removed. Perfect for temporary collaborations. Best Practice: Combine security roles (for general permissions) with scoped capabilities (for record-specific access). The guest user receives their email automatically in Step 1, and these additional capabilities further refine their access.

For scenarios with multiple guest users (like client portals), use dynamic scoping with current_user_id to avoid creating per-user capabilities.

// Tag entities with authorized user IDs
const tagProjectForClients = async (projectId, clientUserIds) => {
  const response = await fetch(`/api/v1/projects/${projectId}`, {
    method: 'PUT',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      project: {
        custom_fields: {
          client_user_ids: clientUserIds  // Array of user IDs
        }
      }
    })
  });

  return response.json();
};

// Create capability with dynamic filtering
const createDynamicClientCapability = async () => {
  const response = await fetch('/api/v1/capabilities', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      capability: {
        action: 'view',
        resource: 'Project',
        description: 'View projects assigned to current user',
        scope: {
          where: {
            'custom_fields': {
              'client_user_ids': {
                'in': ['current_user_id']  // Dynamic resolution
              }
            }
          }
        }
      }
    })
  });

  return response.json();
};

// Create security role for all clients
const setupClientPortal = async () => {
  // 1. Create role
  const roleResponse = await fetch('/api/v1/security_roles', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      security_role: {
        name: 'Client Portal Access',
        description: 'Guest client access to their projects'
      }
    })
  });
  const role = (await roleResponse.json()).security_role;

  // 2. Create dynamic capabilities
  const projectCap = await createDynamicClientCapability();

  // 3. Assign capability to role
  await fetch('/api/v1/capability_assignments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      capability_assignment: {
        capability_id: projectCap.capability.id,
        accessor_record_type: 'SecurityRole',
        accessor_record_id: role.id
      }
    })
  });

  return role;
};

// Assign role to each client
const assignClientToRole = async (roleId, clientUserId) => {
  await fetch('/api/v1/security_role_assignments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      security_role_assignment: {
        security_role_id: roleId,
        accessor_record_type: 'User',
        accessor_record_id: clientUserId
      }
    })
  });
};
Scalability: This pattern scales to hundreds of clients without creating individual capabilities. Just tag projects and assign the same role to new clients.

Guest users have two conversion paths: upgrading to a full user within the same tenant, or converting to their own tenant owner with automatic cross-tenant access.

Option 1: Convert to Full User

Upgrade a guest to a regular user within your tenant:

// Convert guest to full user in same tenant
const convertToFullUser = async (userId, sendNotification = false) => {
  const response = await fetch(`/api/v1/users/${userId}/convert_to_full_user`, {
    method: 'PUT',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      send_notification: sendNotification
    })
  });

  const data = await response.json();
  // Returns: { success: true, user: {...}, message: "Guest user successfully converted to full user" }
  return data;
};

Option 2: Convert to Tenant Owner

Create a new tenant for the guest user with automatic cross-tenant sharing:

// Convert guest to tenant owner with auto-created sharing relationship
const convertToTenant = async (userId, tenantOptions) => {
  const response = await fetch(`/api/v1/users/${userId}/convert_to_tenant`, {
    method: 'PUT',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      tenant_options: {
        name: tenantOptions.name,
        enterprise_identifier: tenantOptions.enterpriseId,
        shared_key: tenantOptions.sharedKey,           // Optional
        product_type_id: tenantOptions.productTypeId,   // Optional, defaults to 'free'
        pricing_plan_id: tenantOptions.pricingPlanId    // Optional, defaults to 'free'
      }
    })
  });

  const data = await response.json();
  // Returns:
  // {
  //   success: true,
  //   user: {...},
  //   tenant: {...},
  //   sharing_relationship: {...},  // Auto-created for cross-tenant access
  //   message: "Guest user successfully converted to tenant owner"
  // }
  return data;
};
Automatic Cross-Tenant Sharing: When converting to tenant owner, a sharing relationship is automatically created between the new tenant and your tenant. This allows the converted user to switch between both workspaces seamlessly. After Conversion:
  • Full User: User remains in your tenant with full access (no longer flagged as guest)
  • Tenant Owner: User gets their own tenant AND maintains access to your tenant via auto-created sharing relationship
Complete Example: See the Tenant Sharing Documentation for detailed scenarios including contractor onboarding (Doug + Rick) and client portal setup (Whit + Multiple Clients).

Cross-Tenant User Access System

mdi-link
Overview

The Cross-Tenant User Access system enables controlled user-level access between tenants. Users from one tenant can be granted specific roles and permissions in another tenant, enabling collaboration while maintaining security and data isolation.

Key Features

mdi-check-circle Request/Accept Flow: Formal relationship establishment between tenants mdi-check-circle User-Based Access: Grant specific users from other tenants access mdi-check-circle Role-Based Permissions: Control access through SecurityRoles and Capabilities mdi-check-circle Workspace Switching: Seamlessly switch between tenant workspaces mdi-check-circle Time-bound Access: Optional start/end dates for relationships
Core Concepts

mdi-handshake Tenant Relationships

Formal relationships between tenants with statuses: pending (awaiting acceptance), active (functional), suspended (temporary pause - maintains roles), revoked (permanent - removes all roles).

mdi-information Suspend for temporary pauses, revoke for permanent termination.

mdi-account-multiple User Access Control

Individual users are granted SecurityRoles in the target tenant. Standard RBAC determines what they can view, edit, or delete based on capabilities.

mdi-swap-horizontal Workspaces

Guest users can switch between their own tenant workspace and host workspaces where they've been granted access.

mdi-information Host users only see their own workspace - they provide access, not receive it.

Workflow: Request → Accept → Grant Roles → Access

Step 1: Tenant A requests access for specific users
Step 2: Tenant B accepts the relationship request
Step 3: Tenant B grants SecurityRoles to users from Tenant A
Step 4: Users switch workspaces and access Tenant B's data (within their role permissions)

API Examples Relationships Granting Access Workspaces

Request access for users to another tenant.

// Request access for specific users
const requestAccess = async (targetTenantId, userIds, message) => {
  const response = await fetch('/api/v1/tenant_sharing/request', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      target_tenant_id: targetTenantId,
      description: 'Project collaboration',
      request_message: message,
      user_ids: userIds  // [123, 456] or omit to share only current user
      // OR use: share_with_all_users: true
    })
  });

  const data = await response.json();
  return data; // Returns relationship with status "pending"
};

Accept an incoming relationship request.

// Accept a relationship request
const acceptRequest = async (relationshipId, message) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/accept`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      acceptance_message: message
    })
  });

  const data = await response.json();
  return data; // Returns relationship with status "active"
};

List all tenant relationships for your tenant.

// List all relationships
const listRelationships = async (status = null) => {
  let url = '/api/v1/tenant_sharing';
  if (status) {
    url += `?status=${status}`; // e.g., "party_relationship_active"
  }

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId
    }
  });

  const data = await response.json();
  return data; // Array of relationships
};

Add more users to an existing relationship (requesting tenant only).

// Add users to relationship
const addUsers = async (relationshipId, userIds) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/add_users`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      user_ids: userIds  // [125, 126]
    })
  });

  const data = await response.json();
  return data; // Updated relationship
};

Permanently revoke a tenant relationship and remove all role assignments.

// Revoke a relationship (permanent - requires new invitation to re-establish)
const revokeRelationship = async (relationshipId, reason) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/revoke`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      revocation_reason: reason
    })
  });

  const data = await response.json();
  return data; // Removes all SecurityRoleAssignments permanently
};
Permanent Action: Revoke removes all role assignments. To re-establish access, you must send a new invitation. Use suspend for temporary pauses instead.

Temporarily suspend a relationship without removing role assignments.

// Suspend a relationship (temporary - can be reactivated)
const suspendRelationship = async (relationshipId, reason) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/suspend`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      suspension_reason: reason || 'Temporarily suspended'
    })
  });

  const data = await response.json();
  return data; // Maintains SecurityRoleAssignments for easy reactivation
};
Temporary Pause: Suspend maintains all role assignments. Use reactivate to resume access instantly. Either host or guest tenant can suspend/reactivate.

Reactivate a previously suspended relationship.

// Reactivate a suspended relationship
const reactivateRelationship = async (relationshipId) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/reactivate`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({})  // No body required
  });

  const data = await response.json();
  return data; // Restores access with all previously assigned roles
};
Instant Restoration: Reactivate immediately restores access with all previously assigned roles. No need to reassign roles.

After accepting a relationship, the target tenant must grant SecurityRoles to users.

Get list of users who should be granted access (target tenant only).

// List shareable users
const listShareableUsers = async (relationshipId) => {
  const response = await fetch(`/api/v1/tenant_sharing/${relationshipId}/shareable_users`, {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId  // Must be target tenant
    }
  });

  const data = await response.json();
  // Returns:
  // {
  //   shareable_users: [
  //     {
  //       id: 123,
  //       username: "john_doe",
  //       display_name: "John Doe",
  //       assigned_roles: [ ... ],
  //       has_access: true/false
  //     }
  //   ],
  //   share_with_all_users: false
  // }
  return data;
};

Create a SecurityRole for external users.

// Create a role for partners
const createPartnerRole = async () => {
  const response = await fetch('/api/v1/security_roles', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      security_role: {
        name: 'Partner Viewer',
        description: 'Read-only access for partners'
      }
    })
  });

  const data = await response.json();
  return data.security_role; // Returns role with ID
};

Assign capabilities to a role.

// Assign capabilities to role
const assignCapabilities = async (roleId, capabilityIds) => {
  for (const capId of capabilityIds) {
    await fetch(`/api/v1/security_roles/${roleId}/assign_capability`, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + token,
        'Tenant-Id': currentTenantId,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        capability_id: capId
      })
    });
  }
};

Assign a role to a user from another tenant.

// Assign role to external user
const grantRoleToUser = async (roleId, userId) => {
  const response = await fetch('/api/v1/security_role_assignments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      security_role_assignment: {
        security_role_id: roleId,
        accessor_record_type: 'User',
        accessor_record_id: userId  // User ID from requesting tenant
      }
    })
  });

  const data = await response.json();
  return data.security_role_assignment;
};
Important: Workspaces are for guest users with granted roles only. If you're a host tenant user (inviting others), you will only see your own workspace. If you're a guest user (who was shared), you'll see your own workspace plus host workspaces only after the host has assigned you SecurityRoles. Being in the shared users list is not enough.

Workspaces allow guest users to seamlessly switch between their own tenant's data and host tenants where they have been granted access.

Get all workspaces you can access.

// List all available workspaces
const listWorkspaces = async () => {
  const response = await fetch('/api/v1/workspaces', {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId
    }
  });

  const data = await response.json();

  // HOST TENANT USER - Only sees own workspace:
  // {
  //   workspaces: [
  //     {
  //       workspace_tenant_id: "my-org",
  //       workspace_name: "My Organization",
  //       relationship_id: null,
  //       assigned_roles_count: null,
  //       has_access: true
  //     }
  //   ]
  // }

  // GUEST USER WITH ROLES - Sees own + host workspaces where roles assigned:
  // {
  //   workspaces: [
  //     {
  //       workspace_tenant_id: "my-org",
  //       workspace_name: "My Organization",
  //       relationship_id: null,
  //       assigned_roles_count: null,
  //       has_access: true
  //     },
  //     {
  //       workspace_tenant_id: "partner-org",
  //       workspace_name: "Partner Organization",
  //       relationship_id: 789,
  //       assigned_roles_count: 2,
  //       has_access: true,
  //       role: "guest"
  //     }
  //   ]
  // }

  // GUEST USER WITHOUT ROLES - Only sees own workspace:
  // (Even if they're in shared_users metadata, without role assignments they don't see host workspace)
  // {
  //   workspaces: [
  //     {
  //       workspace_tenant_id: "my-org",
  //       workspace_name: "My Organization",
  //       relationship_id: null,
  //       assigned_roles_count: null,
  //       has_access: true
  //     }
  //   ]
  // }

  return data.workspaces;
};

View your roles and capabilities in a specific workspace.

// Check what access I have
const checkMyAccess = async (workspaceTenantId) => {
  const response = await fetch(`/api/v1/workspaces/${workspaceTenantId}/my_access`, {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': currentTenantId
    }
  });

  const data = await response.json();
  // Returns:
  // {
  //   roles: [
  //     {
  //       role: { id, name, description },
  //       capabilities: [ ... ]
  //     }
  //   ]
  // }
  return data.roles;
};

Access workspace data by setting the Tenant-Id header.

// Access data in another workspace
const getPartnerTasks = async (partnerTenantId) => {
  const response = await fetch('/api/v1/tasks', {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
      'Tenant-Id': partnerTenantId  // Use partner's tenant ID
    }
  });

  const data = await response.json();
  // Returns tasks from partner tenant (filtered by your capabilities)
  return data.tasks;
};

// Important: API enforces your role permissions in that workspace
// You only see/edit what your assigned roles allow
Best Practice: Always check your access in a workspace before attempting operations. Use the my_access endpoint to understand your permissions.
Security Considerations mdi-shield-alert Relationship Required: Users can only access other tenants through accepted relationships mdi-shield-alert Role-Based Access: All access controlled through SecurityRoles and Capabilities mdi-shield-alert User-Level Granularity: Individual users are granted specific roles mdi-shield-alert Audit Trail: All relationship and role assignment operations are logged mdi-shield-alert Revocation: Revoking a relationship removes all SecurityRoleAssignments

Dynamic Functions

mdi-link
Serverless JavaScript Functions

Dynamic Functions provide a serverless JavaScript execution environment within the DSiloed platform. They allow you to create, manage, and execute custom JavaScript functions that can integrate with external APIs, process data, and automate workflows—all without managing any infrastructure.

mdi-shield-lock Sandboxed Execution JavaScript runs in a secure V8 sandbox with memory and CPU limits. Functions are tenant-isolated with no filesystem access. mdi-key OAuth Integration Configuration-based OAuth 2.0 support for any provider. Automatic token refresh keeps your integrations running. mdi-clock-outline Scheduled Execution Use cron expressions to schedule functions to run automatically. Perfect for data syncs, reports, and automated workflows. mdi-webhook Model Callbacks Trigger functions automatically when records are created, updated, or deleted. Build event-driven automation workflows. mdi-database Data Access Full access to tenant data via executeAction. Query, create, update, and delete records from your functions. mdi-robot MCP Integration AI agents can manage and execute functions via MCP tools. Empower your AI to automate complex workflows.
Creating Your First Function

Create a dynamic function using the REST API. Functions receive a context object with parameters, configuration, OAuth tokens, and tenant information.

Tip: Function names must be 1-128 characters using alphanumeric characters, underscores, or hyphens.
// Create a function via REST API
POST /api/v1/dynamic_functions
Content-Type: application/json

{
  "dynamic_function": {
    "name": "hello_world",
    "description": "A simple greeting function",
    "javascript_code": "return { message: 'Hello, ' + (params.name || 'World') + '!' };",
    "active": true,
    "timeout_seconds": 30
  }
}

Execute the function by ID or name:

// Execute by name
POST /api/v1/dynamic_functions/hello_world/execute
Content-Type: application/json

{
  "params": {
    "name": "DSiloed"
  }
}

// Response:
{
  "success": true,
  "result": { "message": "Hello, DSiloed!" },
  "execution_id": 456,
  "execution_time_ms": 12
}
JavaScript Execution Context

Functions receive a rich context with access to parameters, configuration, and built-in functions for data access and HTTP requests.

// Available context properties
params                    // Parameters passed when executing
config                    // Function configuration (excluding secrets)
functionId                // Database ID of this function
functionName              // Function name
tenantId                  // Current tenant ID

// OAuth tokens (if connected)
oauth.accessToken         // OAuth access token
oauth.tokenType           // Token type (usually "Bearer")

// Callback context (only in callback executions)
event                     // 'create', 'update', or 'destroy'
model_name                // Model class name (e.g., 'Task')
record_id                 // ID of the affected record
record                    // Full record data
changes                   // Changed attributes (update only)
// Make HTTP requests to external APIs
const response = await fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${oauth.accessToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ key: 'value' })
});

const data = await response.json();
return { fetched: data };
// Query tenant data
var customers = executeAction('Individual', 'list', {
  filters: { role_type_id: 'customer' },
  limit: 100
});

// Create records
var newContact = executeAction('PhoneNumber', 'create', {
  attributes: {
    party_id: 123,
    phone_number: '555-1234'
  }
});

// Update records
executeAction('Individual', 'update', {
  id: 456,
  attributes: { custom_fields: { synced: true } }
});

// Convenience wrappers also available:
listRecords(modelName, options)
getRecord(modelName, id)
createRecord(modelName, attributes)
updateRecord(modelName, id, attributes)
deleteRecord(modelName, id)
findOrCreateRecord(modelName, findAttrs, createAttrs)
// Execute another DynamicFunction by name (synchronous by default)
const result = executeFunction('helper_function', { key: 'value' });

if (result.success) {
  console.log('Helper returned:', result.result);
} else {
  console.error('Helper failed:', result.error);
}

// Execute asynchronously (queued for background execution)
const asyncResult = executeFunction('long_running_task', { data: params.input }, true);
// Returns immediately: { success: true, message: '...', execution_id: ... }

// Chain multiple functions together
const validation = executeFunction('validate_data', { data: params.input });
if (!validation.success || !validation.result.valid) {
  return { success: false, error: 'Validation failed' };
}

const processed = executeFunction('process_data', {
  data: params.input,
  validationResult: validation.result
});

return processed.result;

// executeFunction(functionNameOrId, params, async)
// - functionNameOrId: Name or ID of the function
// - params: Parameters object (optional)
// - async: If true, queues for background execution (optional, default: false)
// Look up external ID for a local record
var result = getExternalId('Party', partyId, 'xero', 'xero_user_id');
if (result.found) {
  console.log('Xero user ID:', result.external_id);
}

// Find local record by external ID
var projectMapping = findByExternalId('Project', 'xero', xeroProjectId);
if (projectMapping.found) {
  var localProject = projectMapping.record;
}

// Store a new mapping
storeExternalMapping('Project', localProjectId, 'xero', xeroProjectId, {
  column_name: 'xero_project_id',
  table_name: 'xero_projects',
  is_primary_key: true
});
Scheduling Functions

Functions can be scheduled to run automatically using cron expressions. The scheduler checks every 30 seconds for functions due to execute.

Schedule Cron Expression
Every minute* * * * *
Every hour0 * * * *
Daily at 9am0 9 * * *
Every Monday at 8am0 8 * * 1
First of month at midnight0 0 1 * *
Every 15 minutes*/15 * * * *
Required: Scheduled functions must specify run_as_user_id to define which user's permissions are used during background execution. The user must belong to the same tenant.
// Create a scheduled function
POST /api/v1/dynamic_functions
{
  "dynamic_function": {
    "name": "daily_sync",
    "description": "Sync contacts with CRM every morning",
    "javascript_code": "/* sync logic */",
    "schedule": "0 9 * * *",
    "timeout_seconds": 120,
    "run_as_user_id": 123,
    "active": true
  }
}
Model Lifecycle Callbacks

Functions can be triggered automatically when records are created, updated, or destroyed. This enables event-driven automation without polling.

Supported Models

Individual Organization Party Task Project TimeEntry BizTxnEvent BizTxnAccount

Callback Events

create update destroy all
Required: Callback functions must specify run_as_user_id to define which user's permissions are used during callback execution. The user must belong to the same tenant.
// Create a callback function for task creation
POST /api/v1/dynamic_functions
{
  "dynamic_function": {
    "name": "on_task_created",
    "description": "Notify team when tasks are created",
    "callback_model": "Task",
    "callback_event": "create",
    "run_as_user_id": 123,
    "javascript_code": `
      // Context includes: event, model_name, record_id, record
      console.log('Task created:', record.name);

      // Send notification to external system
      await fetch('https://slack.com/api/chat.postMessage', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + oauth.accessToken,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          channel: '#tasks',
          text: 'New task created: ' + record.name
        })
      });

      return { notified: true };
    `
  }
}
OAuth Integration

Dynamic Functions support OAuth 2.0 for integrating with external services. OAuth is configuration-based, meaning any OAuth 2.0 provider can be used without code changes.

OAuth Configuration

{
  "oauth": {
    "provider": "xero",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "authorize_url": "https://login.xero.com/identity/connect/authorize",
    "token_url": "https://identity.xero.com/connect/token",
    "scopes": ["openid", "profile", "accounting.transactions.read"]
  }
}

OAuth Flow

1. Configure - Set OAuth config on the function 2. Authorize - Call /oauth/authorize to get authorization URL 3. Redirect - User authorizes access at the provider 4. Callback - Provider redirects with authorization code 5. Token Exchange - System exchanges code for tokens 6. Usage - Access tokens via oauth.accessToken Auto-refresh: Tokens are automatically refreshed before execution when they are expired or expiring within 5 minutes.
REST API Reference
Endpoint Description
GET /api/v1/dynamic_functions List all functions
POST /api/v1/dynamic_functions Create a new function
GET /api/v1/dynamic_functions/:id Get function details
PUT /api/v1/dynamic_functions/:id Update a function
DELETE /api/v1/dynamic_functions/:id Delete a function
POST /api/v1/dynamic_functions/:id/execute Execute a function (sync or async)
GET /api/v1/dynamic_functions/:id/export Export function configuration
POST /api/v1/dynamic_functions/import Import a function
POST /api/v1/dynamic_functions/:id/oauth/authorize Start OAuth authorization
GET /api/v1/dynamic_functions/:id/oauth/status Check OAuth connection status
POST /api/v1/dynamic_functions/:id/oauth/disconnect Disconnect OAuth
Tip: Functions can be referenced by ID (numeric) or name (string) in URLs. For example: /api/v1/dynamic_functions/my_function/execute
MCP Tools for AI Agents

AI agents can manage and execute Dynamic Functions via five dedicated MCP tools. This enables intelligent automation where AI can create, modify, and run custom code.

list_dynamic_functions_tool List functions with filtering by active status, schedule, callbacks, and name. get_dynamic_function_tool Get full function details including JavaScript code and execution history. execute_dynamic_function_tool Execute a function by ID or name with parameters (sync or async). manage_dynamic_function_tool Create, update, or delete functions. Supports scheduling and callbacks. Use run_as_user_id: "current_user" for scheduled/callback functions. dynamic_function_executions_tool View execution history with filtering by function, trigger type, and status.
Example: Xero Integration Function

Here's a complete example of a function that syncs time entries to Xero, demonstrating OAuth, data access, and MDM mapping helpers.

// Function: sync_time_entries_to_xero
// Schedule: 0 * * * * (every hour)

// Get unsynced time entries
var entries = listRecords('TimeEntry', {
  filters: { status: 'approved' },
  limit: 50
});

var synced = 0;
var skipped = 0;

for (var entry of entries.data) {
  // Skip if already synced
  var existingMapping = getExternalId('TimeEntry', entry.id, 'xero', 'xero_timeentry_id');
  if (existingMapping.found) {
    skipped++;
    continue;
  }

  // Look up Xero user ID for this party
  var userMapping = getExternalId('Party', entry.party_id, 'xero', 'xero_user_id');
  if (!userMapping.found) {
    console.log('No Xero user mapping for party:', entry.party_id);
    continue;
  }

  // Create time entry in Xero
  var xeroResponse = await fetch('https://api.xero.com/projects/timeEntries', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + oauth.accessToken,
      'Content-Type': 'application/json',
      'Xero-Tenant-Id': config.xero_tenant_id
    },
    body: JSON.stringify({
      userId: userMapping.external_id,
      projectId: entry.custom_fields.xero_project_id,
      duration: entry.hours * 60,
      description: entry.description
    })
  });

  var xeroEntry = await xeroResponse.json();

  // Store mapping for future reference
  storeExternalMapping('TimeEntry', entry.id, 'xero', xeroEntry.timeEntryId, {
    column_name: 'xero_timeentry_id'
  });

  synced++;
}

return {
  success: true,
  synced: synced,
  skipped: skipped,
  message: `Synced ${synced} entries, skipped ${skipped} already synced`
};

Communications Hub

mdi-link

mdi-forum What is the Communications Hub?

The Communications Hub connects Harmoniq conversations to external messaging platforms — Microsoft Teams, Slack, Discord, Twilio SMS, and Twilio Voice. Once configured, messages flow bidirectionally: when someone sends a message in Teams or Slack, it appears in the linked Harmoniq conversation. When an AI agent or user replies in Harmoniq, the response is delivered back to the external platform.

This enables powerful workflows: a customer can text your Twilio number and get an AI-powered response, your team can interact with AI agents directly in their Teams or Slack channels, and all conversations are centralized in Harmoniq with full history, analytics, and agent capabilities.

Multi-tenant by design: Each tenant gets their own isolated channel instances with their own credentials. A single Hub deployment serves all tenants securely.

How it works

Step What happens
1Platform setup — Register a bot or app on the external platform (Teams, Slack, Discord, or Twilio)
2Store credentials — Save the platform credentials in your Harmoniq Configuration settings
3Hub connects — The Hub automatically detects new credentials and connects to the platform
4Link channels — Use the Harmoniq UI to link external channels/conversations to Harmoniq conversations
5Messages flow — Inbound and outbound messages are automatically routed between platforms and Harmoniq

Supported channels

{{ ch.icon }}
{{ ch.name }}
{{ ch.desc }}

mdi-identifier Finding Your Tenant ID

Throughout this guide, webhook URLs include your tenant ID. This is the numeric ID of your tenant in Harmoniq. You'll need it when configuring webhook URLs on external platforms.

Your tenant ID is visible in the Harmoniq admin area, or you can find it by calling the API:

curl -H "Authorization: Bearer YOUR_JWT" \
     -H "Content-Type: application/json" \
     https://www.dsiloed.com/api/v1/parties?role_type_id=tenant

Look for your tenant in the response — the id field is your tenant ID.

Important: All webhook URLs in this guide use the format https://hub.portablemind.ai/webhooks/<channel>/<tenant_id>/.... Replace <tenant_id> with your actual tenant ID (e.g., 22).

mdi-robot AI Agent Display Names

When AI agents reply from Harmoniq, the Hub can display the agent's name instead of the generic bot name on platforms that support it (currently Slack). For example, if you have an agent named "Dilbert", their replies in Slack will show "Dilbert" as the sender rather than "Portablemind Bot".

This happens automatically — no extra configuration needed. The agent's display name is passed through with the outbound message.

mdi-microsoft Microsoft Teams Setup

Connecting Microsoft Teams requires creating an Azure Bot and a Teams App Package. This is the most involved setup of the five channels, but each step is straightforward.

Step 1: Create an Azure App Registration

If you already have an Azure App Registration for a bot, skip to Step 2.
  1. Go to Azure Portal → App registrations
  2. Click New registration
  3. Name: Choose something descriptive (e.g., "Harmoniq Bot")
  4. Supported account types: Accounts in any organizational directory (Multitenant)
  5. Redirect URI: Leave blank for now
  6. Click Register
  7. On the app's Overview page, copy these two values — you'll need them later:
    Application (client) IDThis is your bot_id
    Directory (tenant) IDThis is your Azure tenant_id

Step 2: Create a Client Secret

  1. In your app registration, go to Certificates & secrets
  2. Click New client secret
  3. Add a description (e.g., "Harmoniq Hub") and choose an expiry period
  4. Click Add
  5. Copy the Value immediately — it will only be shown once. This is your bot_password.
Set a reminder to rotate the client secret before it expires. When it expires, your Teams integration will stop working.

Step 3: Create an Azure Bot Resource

  1. Go to Azure Portal → Create a resource → Azure Bot
  2. Bot handle: Choose a unique name
  3. Pricing tier: Free (F0) is fine for getting started
  4. Type of App: Multi Tenant
  5. Creation type: Use existing app registration
  6. App ID: Paste your Application (client) ID from Step 1
  7. Click Review + create, then Create

Step 4: Set the Messaging Endpoint

  1. Open your Azure Bot resource
  2. Go to Configuration
  3. Set Messaging endpoint to:
    https://hub.portablemind.ai/webhooks/msteams/YOUR_TENANT_ID/messages
    Replace YOUR_TENANT_ID with your Harmoniq tenant ID.
  4. Click Apply

Step 5: Enable the Teams Channel

  1. In your Azure Bot resource, go to Channels
  2. Click Microsoft Teams
  3. Accept the terms and click Apply

Step 6: Add Graph API Permission (for Channel Discovery)

This permission allows Harmoniq to list your Teams channels when linking conversations.

  1. Go back to your App registrationAPI permissions
  2. Click Add a permissionMicrosoft GraphApplication permissions
  3. Search for and add: Channel.ReadBasic.All
  4. Click Grant admin consent (requires admin privileges)

Step 7: Find Your Team ID

The Team ID is needed for channel discovery. To find it:

  1. In Microsoft Teams, click the (three dots) next to your team name
  2. Select Get link to team
  3. The URL contains your Team ID as the groupId parameter:
    https://teams.microsoft.com/l/team/...?groupId=97d8107a-2d4f-4280-84d2-ed0a3dd0051d&tenantId=...
                                                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                                                              This is your Team ID

If you have multiple teams, comma-separate their IDs.

Step 8: Sideload the Teams App

You need a Teams App Package (a .zip file) to install the bot in your Teams tenant.

  1. Create a manifest.json file with your bot's Application (client) ID
  2. Include two icon files (color icon 192x192, outline icon 32x32)
  3. Zip all three files together
  4. In Teams, go to AppsManage your appsUpload an appUpload a custom app
  5. Select your .zip file and install it

For a complete manifest.json template, see the Teams manifest schema documentation.

Step 9: Store Credentials in Harmoniq

Save the following credentials in your Harmoniq tenant's MS Teams Channel configuration:

Field Value Where to find it
bot_idApplication (client) IDAzure App Registration → Overview
bot_passwordClient secret valueAzure App Registration → Certificates & secrets
tenant_idDirectory (tenant) IDAzure App Registration → Overview
service_urlhttps://smba.trafficmanager.net/teams/Default value — rarely changes
team_idsYour Team ID(s)Teams → Get link to team → groupId
Teams @mention requirement: In team channels, the bot only receives messages when @mentioned. In 1:1 chats, all messages are received automatically.

mdi-chat-processing Slack Setup

Connecting Slack involves creating a Slack App, adding the right permissions, and configuring event subscriptions so Slack notifies the Hub when messages arrive.

Step 1: Create a Slack App

  1. Go to api.slack.com/apps
  2. Click Create New AppFrom scratch
  3. Give your app a name (e.g., "Harmoniq Bot") and select your workspace
  4. Click Create App

Step 2: Add Bot Token Scopes

Go to OAuth & Permissions in the sidebar, scroll to Scopes, and add these Bot Token Scopes:

Scope Purpose
chat:writeSend messages to channels
chat:write.customizeDisplay AI agent names instead of bot name on replies
channels:readList public channels (for channel discovery in Harmoniq)
channels:historyRead messages in public channels the bot is in
groups:readList private channels (for channel discovery)
groups:historyRead messages in private channels the bot is in
im:historyRead direct messages to the bot
files:readAccess files shared in conversations
users:readResolve user information

Step 3: Install the App to Your Workspace

  1. Still on OAuth & Permissions, scroll up and click Install to Workspace
  2. Review the permissions and click Allow
  3. Copy the Bot User OAuth Token (starts with xoxb-) — this is your slack_bot_token

Step 4: Get the Signing Secret

  1. Go to Basic Information in the sidebar
  2. Under App Credentials, copy the Signing Secret — this is your slack_signing_secret

Step 5: Enable Event Subscriptions

  1. Go to Event Subscriptions in the sidebar
  2. Toggle it ON
  3. Set the Request URL to:
    https://hub.portablemind.ai/webhooks/slack/YOUR_TENANT_ID/events
    Slack will send a verification challenge — the Hub responds automatically. You should see a green "Verified" checkmark.
  4. Expand Subscribe to bot events and click Add Bot User Event for each:
    EventDescription
    message.channelsMessages in public channels the bot is in
    message.groupsMessages in private channels the bot is in
    message.imDirect messages to the bot
  5. Click Save Changes
Without these events, inbound messages will not work. Outbound messages (Harmoniq → Slack) will still function, but Slack won't notify the Hub when users send messages in channels.

Step 6: Store Credentials in Harmoniq

Save the following in your tenant's Slack Channel configuration:

Field Value Where to find it
slack_bot_tokenBot User OAuth Token (xoxb-...)OAuth & Permissions page
slack_signing_secretSigning SecretBasic Information → App Credentials

Step 7: Invite the Bot to Channels

Important: You can link any visible Slack channel to a Harmoniq conversation in the UI, but the bot can only send and receive messages in channels where it has been invited.

To invite the bot to a channel:

  1. Open the Slack channel
  2. Type /invite @YourBotName
  3. The bot will now receive messages and can respond in that channel

Recommended workflow: Link the channel in Harmoniq first, then invite the bot in Slack. Unlike Teams, the bot receives all messages in a channel — no @mention required.

mdi-gamepad-variant Discord Setup

Discord integration connects text channels in your Discord server to Harmoniq conversations. The bot listens for messages via Discord's Gateway (WebSocket) connection.

Step 1: Create a Discord Application

  1. Go to Discord Developer Portal
  2. Click New Application
  3. Name it (e.g., "Harmoniq Bot") and click Create

Step 2: Add a Bot

  1. Go to the Bot section in the sidebar
  2. Click Reset Token and copy the token immediately — this is your bot_token
  3. Under Privileged Gateway Intents, enable Message Content Intent
  4. Click Save Changes
The bot token is only shown once when you reset it. If you lose it, you'll need to reset it again (which invalidates the old token).

Step 3: Invite the Bot to Your Server

  1. Go to OAuth2URL Generator in the sidebar
  2. Under Scopes, check bot
  3. Under Bot Permissions, check:
    • Send Messages
    • Read Message History
    • View Channels
  4. Copy the generated URL at the bottom, open it in your browser, and select the server to add the bot to

Step 4: Get the Server (Guild) ID

  1. In Discord, go to User SettingsAdvanced → enable Developer Mode
  2. Right-click on your server name in the sidebar
  3. Click Copy Server ID — this is your guild_id

Step 5: Store Credentials in Harmoniq

Field Value Where to find it
bot_tokenBot tokenDeveloper Portal → Bot → Reset Token
guild_idServer (Guild) IDRight-click server → Copy Server ID
Unlike Slack and Teams, the Discord bot receives all messages in all text channels it has access to — no invite or @mention required.

mdi-cellphone-message Twilio SMS Setup

Twilio SMS enables your Harmoniq conversations to send and receive text messages. When someone texts your Twilio number, the message appears in the linked Harmoniq conversation. If no conversation is linked, one is created automatically.

Step 1: Get Your Twilio Credentials

  1. Log in to the Twilio Console
  2. On the dashboard, find and copy:
    Account SIDStarts with AC — this is your twilio_account_sid
    Auth TokenClick to reveal — this is your twilio_auth_token

Step 2: Get a Phone Number

  1. In Twilio Console, go to Phone NumbersManageBuy a number
  2. Choose a number with SMS capability
  3. Note the number in E.164 format (e.g., +18005551234) — this is your twilio_phone_number

Step 3: Configure Twilio Webhooks

  1. Go to Phone NumbersManageActive Numbers → click your number
  2. Under Messaging:
    SettingValue
    A message comes inWebhook, POST, https://hub.portablemind.ai/webhooks/twilio/YOUR_TENANT_ID/sms/inbound
    Status callback URLhttps://hub.portablemind.ai/webhooks/twilio/YOUR_TENANT_ID/sms/status
  3. Click Save

Step 4: Store Credentials in Harmoniq

Field Value Where to find it
twilio_account_sidAccount SID (AC...)Twilio Console → Dashboard
twilio_auth_tokenAuth TokenTwilio Console → Dashboard
twilio_phone_numberE.164 format (e.g., +18005551234)Twilio Console → Phone Numbers
domainhub.portablemind.aiHostname only — no https://
The domain must be the hostname only (e.g., hub.portablemind.ai), not a full URL. The Hub adds https:// automatically.

Auto-Created Conversations

When an SMS arrives from a phone number that isn't linked to any Harmoniq conversation, the system automatically creates a new conversation and links it. The conversation is named twilio-sms-+18005551234 with the phone number in the description. Future messages from the same number route to the same conversation.

Outbound SMS Registration Requirements

US carriers require registration for outbound SMS. Without it, outbound messages may be blocked.

Local numbers (10DLC): Requires A2P campaign registration (Brand + Campaign) — 3-7 business days
Toll-free numbers: Requires toll-free verification — 1-3 business days

Inbound SMS works immediately regardless of registration status. Trial accounts can only send to verified caller IDs.

mdi-phone Twilio Voice Setup

Twilio Voice provides an AI-powered voice agent that answers inbound calls with real-time speech recognition and text-to-speech. The agent can look up caller context from Harmoniq (who is calling, their history, open projects) and perform mid-call data lookups triggered by natural language.

Voice uses the same Twilio account and phone number as SMS. If you've already set up SMS, you just need to add the voice webhook and a few extra configuration values.

Step 1: Configure the Voice Webhook

  1. In Twilio Console, go to Phone NumbersManageActive Numbers → click your number
  2. Under Voice & Fax:
    SettingValue
    A call comes inWebhook, HTTP POST, https://hub.portablemind.ai/webhooks/twilio/YOUR_TENANT_ID/voice/twiml
  3. Click Save

Step 2: Get an OpenAI API Key

The voice agent uses a fast OpenAI model (GPT-4o-mini by default) for real-time responses during calls. This is separate from the LLM models used in Harmoniq conversations.

  1. Go to OpenAI API Keys
  2. Create a new key and copy it

Step 3: Store Credentials in Harmoniq

Save the following in your tenant's Twilio Voice Channel configuration:

Field Value Notes
twilio_account_sidAccount SIDSame as SMS
twilio_auth_tokenAuth TokenSame as SMS
twilio_phone_numberPhone numberSame as SMS (E.164 format)
domainhub.portablemind.aiHostname only
openai_api_keyYour OpenAI API keyFor fast voice responses
openai_modelgpt-4o-miniDefault — fast and cost-effective
tts_providerElevenLabsText-to-speech provider
tts_voice_idElevenLabs voice IDOptional — uses default voice if empty
welcome_greetingYour greeting messageWhat the bot says when answering
context_providerharmoniqEnables caller context enrichment

Voice + SMS Shared Conversations

Voice and SMS share the same conversation for a given phone number. When a caller's phone number is mapped to an SMS conversation, voice calls from that number will load the same conversation context and post transcripts to the same thread.

Caller Context Enrichment

When context_provider is set to harmoniq, the voice agent automatically looks up the caller's phone number in Harmoniq to find who they are, what projects they're associated with, and any relevant history. This context is injected into the AI prompt so the agent can greet the caller by name and have informed conversations.

mdi-link-variant Linking Channels to Conversations

After setting up credentials, you link external channels to specific Harmoniq conversations. This determines which conversation receives messages from which external channel.

  1. Open a conversation in Harmoniq
  2. Click the Channel Settings icon
  3. Go to the External Channels tab
  4. Click Add Mapping
  5. Select Communications Hub as the external system
  6. Choose the resource type:
    Resource TypeHow it works
    Discord ChannelsSearch and select a channel from your Discord server
    Slack ChannelsSearch and select a channel from your Slack workspace
    Teams ConversationsSearch and select a channel from your Teams team
    SMS ConversationsEnter a phone number in E.164 format (e.g., +18005551234)
  7. Save the mapping

Cross-Channel Bridging

A single Harmoniq conversation can be linked to multiple external channels across different platforms. When a message arrives on any linked channel, it is fanned out to all other linked channels on the same conversation. This means a Discord message will appear in Teams and Slack, and vice versa.

Webhook URL Reference

These are the webhook URLs used by each channel (replace YOUR_TENANT_ID with your actual tenant ID):

Channel Webhook URL Where to configure
Teams https://hub.portablemind.ai/webhooks/msteams/YOUR_TENANT_ID/messages Azure Bot → Configuration → Messaging endpoint
Slack https://hub.portablemind.ai/webhooks/slack/YOUR_TENANT_ID/events Slack App → Event Subscriptions → Request URL
Discord No webhook needed — Discord uses a Gateway (WebSocket) connection
Twilio SMS https://hub.portablemind.ai/webhooks/twilio/YOUR_TENANT_ID/sms/inbound Twilio Console → Phone Number → Messaging
Twilio Voice https://hub.portablemind.ai/webhooks/twilio/YOUR_TENANT_ID/voice/twiml Twilio Console → Phone Number → Voice

mdi-wrench Troubleshooting

Problem Likely cause Fix
Outbound works but inbound doesn't (Slack) Bot events not subscribed Add message.channels, message.groups, message.im in Event Subscriptions
Outbound works but inbound doesn't (Teams) Messaging endpoint not set or wrong tenant ID Check Azure Bot → Configuration → Messaging endpoint matches your tenant ID
Bot doesn't respond in Slack channel Bot not invited to channel Type /invite @BotName in the Slack channel
Bot doesn't respond in Teams channel Bot not @mentioned @mention the bot in team channels. 1:1 chats don't need @mentions.
No channels found in "Add Mapping" dialog Credentials not configured or Hub hasn't refreshed Verify credentials are saved in Harmoniq. The Hub refreshes every 5 minutes, or restart the Hub.
Slack shows "Your request URL responded with an HTTP error" Wrong webhook URL format Use the tenant-scoped URL: .../webhooks/slack/YOUR_TENANT_ID/events
SMS outbound blocked (error 30034) A2P registration not complete Complete 10DLC campaign registration or toll-free verification in Twilio
Slack messages show <@U12345> characters Hub version outdated Update the Hub — current versions clean Slack markup automatically
Azure client secret expired Teams integration stopped working Create a new client secret in Azure, update the bot_password in Harmoniq, restart the Hub

Sample Applications

mdi-link
Available Sample Apps

DSiloed comes with several sample applications that demonstrate different capabilities of the platform. Each app is built as a static JavaScript application that interacts with the API.

{{ app.name }}

{{ app.description }}

{{ feature }}
Open App
App Architecture

Each sample application follows a similar architecture pattern:

Static HTML/CSS/JavaScript Apps are built with HTML, CSS, and JavaScript without a server-side component. Vue.js + Vuetify Front-end built with Vue.js and styled with Vuetify components. RESTful API Integration Apps communicate with the DSiloed backend using RESTful endpoints. JWT Authentication Authentication managed through JWT tokens stored in localStorage. Responsive Design Applications are designed to work on desktop and mobile devices.

Building Your Own App

mdi-link
Getting Started with App Development

Building your own application on top of DSiloed is straightforward. You can use any modern JavaScript framework that can make HTTP requests to the API.

Vue.js + Vuetify React + Material UI

Step 1: Create a directory structure for your Vue app

Create a new project directory with the following structure. You can place this directory anywhere on your system or web server:

# Create the main app directory
mkdir -p myapp

# Create subdirectories for CSS, JavaScript, and documentation
mkdir -p myapp/css
mkdir -p myapp/js
mkdir -p myapp/js/components
mkdir -p myapp/docs

# Create basic files
touch myapp/index.html
touch myapp/css/styles.css
touch myapp/js/app.js

mdi-information Note: When deploying your app, make sure to place it in a location accessible to your web server. If you're using this app with DSiloed, you might place it in a directory like /apps/myapp/.

Step 2: Download Essential Components

Next, download these essential components and place them in your application directory structure:

auth.js Authentication utilities Core authentication utility for handling JWT tokens, login state, and session management.

Place in: myapp/js/auth.js
Download
LoginDialog.js Login UI component Ready-to-use login dialog component for handling user authentication and tenant selection.

Place in: myapp/js/components/LoginDialog.js
Download
Documentation API & development guides Comprehensive documentation for building applications with DSiloed, organized into focused topics.

Download and extract docs.zip to: myapp/docs/
CLAUDE.md Download Docs
ChatAssistant.js AI chat component Reusable chat assistant component for adding AI-powered chat to any application.

Place in: myapp/js/components/ChatAssistant.js
Download

Step 3: Set up basic HTML structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My App - DSiloed</title>

    <!-- Vue and Vuetify -->
    <link href="https://cdn.jsdelivr.net/npm/vuetify@3.5.11/dist/vuetify.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">

    <!-- Custom styles -->
    <link rel="stylesheet" href="css/styles.css?v=1771863622">

    <!-- Auth helper -->
    <script src="js/auth.js?v=1771863622"></script>
    <script src="js/components/LoginDialog.js?v=1771863622"></script>

    <!-- App scripts -->
    <script src="https://cdn.jsdelivr.net/npm/vue@3.4.21/dist/vue.global.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@3.5.11/dist/vuetify.min.js"></script>
    <script src="js/app.js?v=1771863622" defer></script>
</head>
<body>
    <div id="app" v-cloak>
        <v-app>
            <v-app-bar color="primary" app>
                <v-app-bar-title>My App</v-app-bar-title>
            </v-app-bar>

            <v-main>
                <v-container>
                    <h1>My Custom App</h1>
                    <p>Welcome to my custom DSiloed application!</p>
                </v-container>
            </v-main>
        </v-app>
    </div>
</body>
</html>

Step 4: Initialize Vue app with authentication

// Initialize Vue app
document.addEventListener('DOMContentLoaded', function() {
  const { createApp } = Vue;
  const { createVuetify } = Vuetify;
  
  // Initialize authentication
  const authManager = new AuthManager('myapp');
  
  // Create Vuetify instance
  const vuetify = createVuetify();
  
  const app = createApp({
    data() {
      return {
        authManager: authManager,
        isAuthenticated: false,
        user: null,
      }
    },
    
    methods: {
      async initialize() {
        // Initialize authentication
        const authResult = await this.authManager.initialize();
        this.isAuthenticated = authResult.authenticated;
        this.user = authResult.user;
        
        // Show login if not authenticated
        if (!this.isAuthenticated) {
          this.showLogin();
        }
      },
      
      showLogin() {
        const loginDialog = new LoginDialog('myapp');
        loginDialog.onLogin = (result) => {
          if (result.success) {
            this.isAuthenticated = true;
            this.user = result.user;
            console.log('Logged in as:', this.user.email);
          }
        };
        loginDialog.show();
      },
      
      logout() {
        this.authManager.logout();
        this.isAuthenticated = false;
        this.user = null;
      }
    },
    
    mounted() {
      this.initialize();
    }
  });
  
  app.use(vuetify);
  app.mount('#app');
});

Step 1: Create a new React app

Start by creating a new React application using Create React App or your preferred tool:

# Create a new React app
npx create-react-app my-model-api-app

# Navigate to the app directory
cd my-model-api-app

# Install Material UI and other dependencies
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

mdi-information Note: You can also use other UI libraries like Chakra UI, Ant Design, or Tailwind CSS if preferred.

Step 2: Set up Authentication Utilities

Create authentication utilities to work with DSiloed:

// src/utils/auth.js
export class AuthManager {
  constructor(appName) {
    this.appName = appName;
    this.authToken = localStorage.getItem('auth_token');
    this.tenantId = localStorage.getItem('tenant_id');
    this.user = null;
  }

  isAuthenticated() {
    return !!this.authToken;
  }

  getToken() {
    return this.authToken;
  }

  getTenantId() {
    return this.tenantId;
  }

  async loadUserInfo() {
    if (!this.isAuthenticated()) {
      return null;
    }

    try {
      const response = await fetch('https://modelapi.russonrails.com/api/v1/users/current', {
        headers: {
          'Authorization': 'Bearer ' + this.authToken,
          'Tenant-Id': this.tenantId || 'root-tenant'
        }
      });

      const data = await response.json();
      
      if (data.success) {
        this.user = data.user;
        return this.user;
      } else {
        this.logout();
        return null;
      }
    } catch (error) {
      console.error('Error loading user info:', error);
      return null;
    }
  }

  async login(email, password, tenantId) {
    try {
      const response = await fetch('https://modelapi.russonrails.com/api/v1/users/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Tenant-Id': tenantId
        },
        body: JSON.stringify({
          email: email,
          password: password
        })
      });

      const data = await response.json();
      
      if (data.success) {
        this.authToken = data.token;
        this.tenantId = tenantId;
        
        localStorage.setItem('auth_token', this.authToken);
        localStorage.setItem('tenant_id', this.tenantId);
        
        await this.loadUserInfo();
        
        return { success: true, user: this.user };
      } else {
        return { success: false, message: data.message || 'Invalid email or password' };
      }
    } catch (error) {
      console.error('Login error:', error);
      return { success: false, message: 'An error occurred during login. Please try again.' };
    }
  }

  logout() {
    this.authToken = null;
    this.user = null;
    localStorage.removeItem('auth_token');
    localStorage.removeItem('tenant_id');
    window.location.href = window.location.pathname;
  }

  async initialize() {
    if (this.isAuthenticated()) {
      const user = await this.loadUserInfo();
      return { authenticated: true, user: user };
    }
    return { authenticated: false };
  }
}

Step 3: Create a Login Dialog Component

// src/components/LoginDialog.jsx
import React, { useState } from 'react';
import {
  Dialog, DialogTitle, DialogContent, DialogActions,
  TextField, Button, CircularProgress, Alert
} from '@mui/material';

function LoginDialog({ open, onClose, onLogin, authManager }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [tenantId, setTenantId] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      if (!tenantId) {
        setError('Tenant ID is required');
        setLoading(false);
        return;
      }

      const result = await authManager.login(email, password, tenantId);

      if (result.success) {
        onLogin(result);
        onClose();
      } else {
        setError(result.message || 'Login failed');
      }
    } catch (err) {
      setError('An unexpected error occurred. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    
      Login to {authManager.appName}
      
{error && {error}} setEmail(e.target.value)} required /> setPassword(e.target.value)} required /> setTenantId(e.target.value)} required />
); } export default LoginDialog;

Step 4: Create Main App Component

// src/App.jsx
import React, { useState, useEffect, useRef } from 'react';
import { 
  Container, AppBar, Toolbar, Typography, Button, 
  ThemeProvider, createTheme, CssBaseline, Box
} from '@mui/material';
import { AuthManager } from './utils/auth';
import LoginDialog from './components/LoginDialog';

const theme = createTheme({
  palette: {
    primary: {
      main: '#3161FF',
    },
    secondary: {
      main: '#6C63FF',
    },
  },
});

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState(null);
  const [loginOpen, setLoginOpen] = useState(false);
  const authManager = useRef(new AuthManager('myapp')).current;
  
  useEffect(() => {
    const initializeAuth = async () => {
      try {
        const authResult = await authManager.initialize();
        setIsAuthenticated(authResult.authenticated);
        setUser(authResult.user);
      } catch (error) {
        console.error('Auth initialization error:', error);
      }
    };
    
    initializeAuth();
  }, []);
  
  const handleLogin = (result) => {
    if (result.success) {
      setIsAuthenticated(true);
      setUser(result.user);
    }
  };
  
  const handleLogout = () => {
    authManager.logout();
    setIsAuthenticated(false);
    setUser(null);
  };
  
  return (
    
      
      
My App {isAuthenticated ? ( <> {user?.email} ) : ( )} {isAuthenticated ? ( My Custom App Welcome to my custom DSiloed application! ) : ( Welcome to My App Please log in to access the application. )} setLoginOpen(false)} onLogin={handleLogin} authManager={authManager} />
); } export default App;

Step 5: Make API Requests

// Example API hook (src/hooks/useApi.js)
import { useState, useCallback } from 'react';

export function useApi(authManager) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const headers = useCallback(() => ({
    'Authorization': 'Bearer ' + authManager.getToken(),
    'Tenant-Id': authManager.getTenantId(),
    'Content-Type': 'application/json'
  }), [authManager]);

  const fetchData = useCallback(async (endpoint, options = {}) => {
    if (!authManager.isAuthenticated()) {
      setError('Authentication required');
      return null;
    }

    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`https://modelapi.russonrails.com/api/v1/${endpoint}`, {
        ...options,
        headers: {
          ...headers(),
          ...(options.headers || {})
        }
      });

      const data = await response.json();

      if (!data.success) {
        throw new Error(data.error?.message || 'API request failed');
      }

      return data;
    } catch (err) {
      setError(err.message);
      return null;
    } finally {
      setLoading(false);
    }
  }, [authManager, headers]);

  return { loading, error, fetchData };
}

// Using the hook in a component:
function PartyList() {
  const [parties, setParties] = useState([]);
  const authManager = useRef(new AuthManager('myapp')).current;
  const { loading, error, fetchData } = useApi(authManager);

  const loadParties = useCallback(async () => {
    const data = await fetchData('parties');
    if (data) {
      setParties(data.parties);
    }
  }, [fetchData]);

  useEffect(() => {
    if (authManager.isAuthenticated()) {
      loadParties();
    }
  }, [authManager, loadParties]);

  if (loading) return ;
  if (error) return {error};

  return (
    
      {parties.map(party => (
        
          
        
      ))}
    
  );
}
Making API Requests Vue.js React

Here's how to make API requests to the DSiloed backend with Vue.js:

// Example method to fetch parties
async fetchParties() {
  try {
    const response = await fetch('https://modelapi.russonrails.com/api/v1/parties', {
      headers: {
        'Authorization': 'Bearer ' + this.authManager.getToken(),
        'Tenant-Id': this.authManager.getTenantId()
      }
    });
    
    const data = await response.json();
    
    if (data.success) {
      this.parties = data.parties;
    } else {
      console.error('Error fetching parties:', data.error);
    }
  } catch (error) {
    console.error('API request failed:', error);
  }
}

Here's how to make API requests to the DSiloed backend with React:

// Using React hooks and fetch API
import { useState, useEffect, useCallback } from 'react';

function PartyList({ authManager }) {
  const [parties, setParties] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchParties = useCallback(async () => {
    if (!authManager.isAuthenticated()) return;
    
    setLoading(true);
    try {
      const response = await fetch('https://modelapi.russonrails.com/api/v1/parties', {
        headers: {
          'Authorization': 'Bearer ' + authManager.getToken(),
          'Tenant-Id': authManager.getTenantId()
        }
      });
      
      const data = await response.json();
      
      if (data.success) {
        setParties(data.parties);
      } else {
        setError(data.error?.message || 'Failed to load parties');
      }
    } catch (error) {
      setError('API request failed: ' + error.message);
    } finally {
      setLoading(false);
    }
  }, [authManager]);

  useEffect(() => {
    fetchParties();
  }, [fetchParties]);

  // UI components to display the data...
}
Adding AI Chat to Your Application

The ChatAssistant component provides a reusable, framework-independent chat interface that you can easily add to any application. It works with Vue.js, React, vanilla JavaScript, or any other framework.

ChatAssistant Features
  • Framework independent - works with any JavaScript application
  • Responsive design with quarter-screen flyout that expands to fullscreen
  • Modern UI with dark mode support
  • Real-time chat with AI assistants
  • Model selection when multiple models are available
  • Automatic conversation management

Basic Usage

// Include the ChatAssistant component
<script src="js/components/ChatAssistant.js?v=1771863622"></script>

// Initialize in your application
const chatAssistant = new ChatAssistant('MyApp', {
    apiClient: apiClient,  // Your API client instance
    title: 'AI Assistant',
    placeholder: 'Ask me anything...',
    contextMessage: 'Assistant has access to your application data'
});

// Show/hide the chat
chatAssistant.toggle();

Vue.js Integration Example

// In your Vue app
methods: {
    initChatAssistant() {
        this.chatAssistant = new ChatAssistant('MyApp', {
            apiClient: apiClient,
            title: 'MyApp Assistant',
            placeholder: 'Ask about your data...',
            contextMessage: 'Assistant can help with your application',
            onError: (error) => {
                this.showSnackbar('Chat error: ' + error.message, 'error');
            }
        });
    },
    
    toggleChat() {
        if (this.chatAssistant) {
            this.chatAssistant.toggle();
        }
    }
},

mounted() {
    this.initChatAssistant();
}

React Integration Example

import { useEffect, useRef } from 'react';

function MyApp() {
    const chatAssistantRef = useRef(null);
    
    useEffect(() => {
        // Initialize ChatAssistant
        chatAssistantRef.current = new ChatAssistant('MyApp', {
            apiClient: apiClient,
            title: 'MyApp Assistant',
            onError: (error) => {
                showErrorToast(error.message);
            }
        });
        
        // Cleanup on unmount
        return () => {
            if (chatAssistantRef.current) {
                chatAssistantRef.current.destroy();
            }
        };
    }, []);
    
    const toggleChat = () => {
        if (chatAssistantRef.current) {
            chatAssistantRef.current.toggle();
        }
    };
    
    return (
        <div>
            <button onClick={toggleChat}>
                Toggle Chat Assistant
            </button>
            {/* Your app content */}
        </div>
    );
}

Configuration Options

const chatAssistant = new ChatAssistant('AppName', {
    // Required
    apiClient: apiClientInstance,
    
    // UI Configuration
    title: 'AI Assistant',
    placeholder: 'Ask me anything...',
    contextMessage: 'Assistant has access to your data',
    position: 'right', // 'left' or 'right'
    width: '400px',
    height: '600px',
    zIndex: 1000,
    
    // Event Callbacks
    onToggle: (isVisible) => {
        console.log('Chat toggled:', isVisible);
    },
    onMessage: (message) => {
        console.log('New message:', message);
    },
    onError: (error) => {
        console.error('Chat error:', error);
    }
});

The ChatAssistant automatically handles authentication using your existing apiClient instance. It will create conversations, manage message history, and provide a seamless chat experience without additional configuration.

LLM-Powered App Development

You can use Large Language Models (LLMs) like Claude to help you build applications on top of DSiloed. Here's a prompt template to help you get started:

LLM Prompt Template

Use this prompt template with Claude or other AI assistants to help build your application. For best results, download and extract the docs.zip file to your myapp/docs/ directory and provide the documentation files to the AI for comprehensive guidance.

Copy Prompt Download Docs.zip

Simply modify the prompt template above to specify your application's requirements, then paste it to an LLM like Claude or GPT to get help building your application.

When using LLMs for app development, provide as much context as possible about your specific requirements and the data models you plan to use. This will help the LLM generate more relevant and usable code.

Next Steps

mdi-link
Continue Your Journey

Now that you've explored the DSiloed platform, here are some next steps to continue your journey:

Create Your Tenant

Set up your tenant environment to begin building applications.
Sign Up

Explore Sample Apps

Review the sample applications to understand the platform's capabilities.
Open Dashboard

Read API Documentation

Understand the API endpoints available for your application.
API Docs

Build Your First App

Create a simple application using the DSiloed platform.
Read Guide

Launch Your Application

Deploy your application and share it with users.
Get Help

Need help with the DSiloed platform? Here are some resources to assist you:

mdi-book-open-variant Documentation

Access comprehensive documentation for the DSiloed platform.

Open Documentation

Code Workspaces

mdi-link

Code Workspaces provide remote development environments that AI agents and developers can interact with over SSH. Each workspace connects to a remote server and provides tools for file operations, git, shell commands, and code search.

mdi-information-outline Overview

A Code Workspace record describes:

Where — hostname, port, SSH username, working directory How — SSH credentials stored in an encrypted Configuration record What — workspace config (allowed commands, timeouts, Docker settings) Who — optional links to a Project and/or LLM Agent
mdi-clipboard-check-outline Prerequisites

Before creating workspaces, your remote server needs:

Requirement Why
SSH user account Workspace tools connect via SSH (Net::SSH)
git installed Provisioning clones repos via git clone
SSH key for git provider The remote user must be able to git clone from your provider (e.g., GitLab, GitHub)
Writable base directory The workspace_base_path must exist and be writable by the SSH user
Docker (optional) Only required if using Docker mode for workspace containers
mdi-numeric-1-circle Step 1: Configure Dev Team Settings

The Dev Team Configuration stores shared settings for all workspaces in your tenant. This is a Configuration record with the internal identifier dev_team.

In the LLM Manager app: Go to the Workspaces tab and click the "Dev Team Config" button to open the configuration dialog.

Required fields:

Field Description Example
ssh_host IP or hostname of your dev server 35.226.181.23
ssh_port SSH port (usually 22) 22
ssh_username User to SSH in as rholmes
ssh_private_key Ed25519 or RSA private key (encrypted at rest) Paste the full private key
workspace_base_path Parent directory for all workspaces /home/rholmes/code-workspaces
git_provider Source control platform gitlab
repo_clone_url Default SSH clone URL for repos git@gitlab.com:model-api/model_api.git

Optional fields:

Field Description Default
docker_enabled Enable Docker mode for shell commands false
docker_container_name Container name when Docker mode is on
allowed_commands Comma-separated allowlist of shell commands bundle, rspec, rubocop, rake, yarn, npm, node, npx, pytest, pip, make, git
test_command Command to run tests
lint_command Command to run linting

API Equivalent

POST /api/v1/configurations
{
  "configuration": {
    "internal_identifier": "dev_team",
    "description": "Dev Team Configuration",
    "is_template": false,
    "config_values": {
      "ssh_host": "35.226.181.23",
      "ssh_port": 22,
      "ssh_username": "rholmes",
      "ssh_private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...",
      "workspace_base_path": "/home/rholmes/code-workspaces",
      "git_provider": "gitlab",
      "repo_clone_url": "git@gitlab.com:model-api/model_api.git",
      "allowed_commands": "bundle,rspec,rubocop,rake,yarn,npm,node,npx,git"
    }
  }
}
mdi-numeric-2-circle Step 2: Create a Workspace

Once the Dev Team Config is saved, you can create workspaces from the Workspaces tab.

In the LLM Manager app: Click the "Add Workspace" button. Fill in a name and optionally override defaults from the Dev Team Config.

Workspace fields:

Field Description Notes
name Friendly workspace name Required. Used to generate the slug.
hostname Server IP or hostname Defaults from Dev Team Config ssh_host
port SSH port Defaults from Dev Team Config ssh_port
ssh_username SSH user Defaults from Dev Team Config
working_directory Path to code on the server Auto-generated: base_path/tenant/workspace/repo
default_branch Git branch Defaults to master
project_id Link to a Project Optional
llm_agent_id Link to an Agent Optional — agent can use workspace tools

API Equivalent

POST /api/v1/code_workspaces
{
  "code_workspace": {
    "name": "Model API Dev",
    "hostname": "35.226.181.23",
    "port": 22,
    "ssh_username": "rholmes",
    "working_directory": "/home/rholmes/code-workspaces/harmoniq/model-api-dev/repo",
    "default_branch": "master",
    "workspace_config": {
      "test_command": "bundle exec rspec",
      "lint_command": "bundle exec rubocop",
      "allowed_commands": ["bundle", "rspec", "rubocop", "rake", "git", "ruby"]
    }
  }
}
mdi-numeric-3-circle Step 3: Store SSH Credentials

Workspaces need SSH credentials to connect to the remote server. If your Dev Team Config includes an ssh_private_key, credentials are created automatically when a workspace is created.

Automatic: If the Dev Team Config has ssh_private_key set, credentials are auto-created as a Configuration record named code_workspace_credentials_{slug}. No manual step needed.

To manually store or update credentials:

In the LLM Manager app: Click the key icon on any workspace row to open the Credentials dialog. Paste the SSH private key and save.

API Equivalent

POST /api/v1/code_workspaces/:id/store_credentials
{
  "ssh_username": "rholmes",
  "ssh_private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n..."
}
Security: Private keys are encrypted at rest using HasEncryptedSecrets. They are never returned in API responses — only a [STORED] placeholder is shown.
mdi-numeric-4-circle Step 4: Test Connection

Verify that Harmoniq can connect to your remote server through the workspace.

In the LLM Manager app: Click the plug icon on any workspace row to test the SSH connection. A success dialog will show the connection status, current directory, and user identity.

API Equivalent

POST /api/v1/code_workspaces/:id/test_connection

Success response:

{
  "success": true,
  "code_workspace": {
    "id": 1,
    "name": "Model API Dev",
    "connection_status": "connected",
    "current_directory": "/home/rholmes/code-workspaces/harmoniq/model-api-dev/repo",
    "user_identity": "rholmes"
  }
}
mdi-numeric-5-circle Step 5: Provision the Workspace (Optional)

Provisioning creates directories on the remote server and clones the repository. This happens automatically when an agent uses the WorkspaceManageTool or WorkspaceProvisionTool, but you can also trigger it manually.

API Equivalent

POST /api/v1/code_workspaces/:id/provision

What provisioning does:

Creates the workspace directory (mkdir -p) Clones the repo from repo_clone_url if configured Starts Docker containers if Docker mode is enabled
mdi-cog-outline Workspace Configuration Reference

All configuration lives in the workspace_config JSON field.

Commands & Timeouts

Key Type Default Description
test_command string Command to run tests
lint_command string Code linting command
build_command string Build command
allowed_commands string[] bundle, rspec, rubocop, rake, yarn, npm, node, npx, pytest, pip, make, git Allowlisted shell commands
shell_timeout_seconds integer 60 Per-command timeout (max 300)
project_instructions string Free-text instructions for agents

Docker Settings

Key Type Default Description
docker_container_name string Enables Docker exec mode when set
docker_user string User for docker exec -u
docker_companion_containers string[] [] Additional containers to manage alongside main
auto_manage_container boolean false Auto-start on use, auto-stop on idle

File Filtering

Key Type Default Description
file_patterns_exclude string[] node_modules, .git, vendor/bundle, tmp, log Patterns excluded from directory listings
file_extensions_include string[] Optional allowlist of file extensions
mdi-docker Docker Mode

When docker_container_name is set, the workspace operates in hybrid Docker mode. Code execution runs inside the Docker container, while file operations run directly on the host via SSH.

Host Operations (direct SSH) File read/write/delete Directory listing Git operations Code search Container Operations (docker exec) Shell commands (bundle, rspec, rake, etc.) Test execution Build processes Any direct exec calls How it works: SSH connects to the remote host. Shell commands are wrapped with docker exec -i [-u user] -w /working/dir container_name bash. File operations use SCP directly to the host filesystem (the repo lives on the host, mounted into the container).
mdi-robot Agent Integration

When a workspace is linked to an LLM Agent, the agent gains access to remote development tools:

RemoteFileTool — Read, write, and delete files on the remote server RemoteDirectoryTool — List directory contents with filtering RemoteGitTool — Git operations (status, diff, commit, push, pull, branch) RemoteSearchTool — Search code with grep/ripgrep patterns RemoteShellTool — Execute allowlisted shell commands WorkspaceManageTool — Create, provision, and manage workspaces Tip: To assign a workspace to an agent, set the llm_agent_id field when creating or editing the workspace. The agent will automatically have access to the workspace tools in its next conversation.
mdi-link

mdi-link-variant What is SiloLink?

SiloLink is a local daemon that bridges Claude Code sessions to Harmoniq conversations. It runs on your machine and exposes an MCP (Model Context Protocol) server that Claude Code connects to, while maintaining a real-time WebSocket connection to the Harmoniq platform.

This enables powerful remote workflows: launch Claude Code sessions from the Harmoniq UI, monitor them from the Agent Dashboard, send commands from Slack or Discord, and receive progress notifications — all without being at the terminal.

Pull-based model: Claude Code must actively call MCP tools to check for messages. SiloLink uses a non-blocking poll loop pattern for reliable message delivery.

How it works

Component Role
Claude CodeRuns in tmux sessions, connects to SiloLink via MCP (Streamable HTTP on localhost:3579)
SiloLinkLocal daemon that bridges MCP calls to Harmoniq via WebSocket (ActionCable)
Claude LauncherSpawns and manages multiple Claude Code tmux sessions (one per conversation)
Control ChannelTenant-scoped ActionCable channel for launch/stop commands from the UI
HarmoniqRoutes messages to the web UI and any linked external channels (Slack, Discord, Teams, SMS)
mdi-cog-outline Setup & Installation

1. Install SiloLink

git clone git@gitlab.com:model-api/silo_link.git
cd silo_link
pnpm install
pnpm build

# Make the CLI globally available
pnpm link --global

2. Configure

Run the interactive setup wizard:

silolink config

This creates ~/.silolink/config.json with the following fields:

Field Description Default
hostHarmoniq server URLhttps://www.dsiloed.com
tenant_idYour tenant identifierharmoniq
shared_keyTenant shared key for JWT authenticationrequired
user_subYour username or emailrequired
mcp_portLocal port for the MCP server3579
reconnect_interval_msBase reconnect delay5000
max_reconnect_attemptsMax reconnect tries20
claude_commandPath to Claude CLI binaryclaude
claude_working_directoryWorking directory for spawned sessionsempty
claude_auto_respawnAuto-respawn sessions on idle/exitfalse
claude_idle_timeout_msIdle threshold before respawn30000
claude_session_promptDefault prompt for spawned sessionsRegister with SiloLink...

3. Start SiloLink

# Foreground (see logs in terminal)
silolink start

# Background (daemon mode)
silolink start --daemon

# Check status
silolink status

# Launch a Claude session manually
silolink launch
silolink launch -p "Work on the auth refactor"

# Stop the daemon (kills all Claude sessions)
silolink stop

4. Health Check

curl http://localhost:3579/health
# { "status": "ok", "sessions": 2, "cable": "connected" }
mdi-connection Connecting Claude Code

Add SiloLink as an MCP server in your Claude Code settings (.claude.json or project .mcp.json):

{
  "mcpServers": {
    "silolink": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "http://localhost:3579/mcp"
      ]
    }
  }
}

Pre-Approving Permissions

To avoid per-call permission prompts, add to .claude/settings.json:

{
  "permissions": {
    "allow": [
      "mcp__silolink__remote_register",
      "mcp__silolink__remote_notify",
      "mcp__silolink__remote_ask",
      "mcp__silolink__remote_poll",
      "mcp__silolink__remote_check_messages",
      "mcp__silolink__remote_wait_for_command",
      "mcp__silolink__remote_sessions",
      "mcp__silolink__remote_unregister"
    ]
  }
}

Or use the wildcard: "mcp__silolink__*"

Skip All Permission Prompts

For a fully autonomous experience, you can launch Claude Code with the --dangerously-skip-permissions flag:

claude --dangerously-skip-permissions

This bypasses all tool permission prompts, not just SiloLink — Claude Code will execute file edits, shell commands, and MCP tool calls without asking.

Use with caution: Make sure you understand what tools and MCP servers are available in your session before using this flag. It grants Claude Code full autonomy over all connected tools, so only use it in environments where you trust everything that is configured.
mdi-toolbox MCP Tools Reference
Tool Description Blocking
remote_register Registers the session with Harmoniq. Creates or attaches to a conversation. Accepts optional conversation_id. No
remote_notify Fire-and-forget message to the linked conversation. Appears in Harmoniq and linked channels. No
remote_ask Posts a question and waits for a reply (or timeout). Use when Claude Code needs human input. Yes
remote_poll Non-blocking check for the next message. Recommended for poll loops — safer than remote_wait_for_command. No
remote_check_messages Non-blocking check for ALL pending inbound messages. Returns all queued messages and clears the queue. No
remote_wait_for_command Blocks and waits for the next inbound message. Prefer remote_poll in a loop instead. Yes
remote_sessions Lists all active Claude Code sessions registered with this SiloLink instance. No
remote_unregister Unregisters the session, unsubscribes from the conversation, and cleans up. No

Tool Examples

Registering a session

// Claude Code calls:
remote_register({ session_name: "model_api" })

// Returns:
{ session_id: "abc-123", conversation_id: 42, conversation_url: "https://www.dsiloed.com/conversations/42" }

Sending a notification

remote_notify({ message: "Tests passed — 142 examples, 0 failures" })

Asking for human input

// Blocks until someone replies
remote_ask({ question: "Should I proceed with the migration?", timeout: 300 })

// Returns:
{ response: "Yes, go ahead", sender_name: "rholmes" }

Waiting for commands (listening loop)

// Blocks until a message arrives or timeout
remote_wait_for_command({ timeout: 1800 })

// Returns:
{ message: "Run the test suite", sender_name: "rholmes" }
mdi-rocket-launch Claude Session Launcher

SiloLink can spawn and manage Claude Code sessions automatically using tmux. Each conversation gets its own Claude process.

Launch from Harmoniq UI

In TeamChat, find the SiloLinks section and click + Enter a session name and optional prompt, click Launch Session Conversation appears immediately — "Starting session..." shows while Claude initializes (~20-30s) Claude sends "Session ready!" and enters the poll loop

Auto-Launch on Inbound Message

When a message arrives on a SiloLink conversation with no active session, SiloLink automatically posts "Restarting session..." and spawns a new Claude process. The original message is buffered and delivered once Claude registers.

Multi-Session Support

Each conversation gets its own tmux session. Multiple sessions run simultaneously and independently.

# List active tmux sessions
tmux list-sessions | grep silolink

# Attach to watch Claude work
tmux attach -t silolink-claude-1774363497579

# Detach: Ctrl+B, D

Managing Sessions

Send messages in the conversation — Claude polls and responds Gear icon → Delete SiloLink stops the Claude session and deletes the conversation Sessions auto-restart when you send a message to a conversation with no active session
mdi-api Control Channel API

SiloLink subscribes to a tenant-scoped ActionCable control channel on startup. The Harmoniq UI sends commands via REST API:

Endpoint Method Description
/api/v1/silolink/launchPOSTLaunch a new Claude session (creates conversation, broadcasts launch)
/api/v1/silolink/stopPOSTStop a session (by conversation_id or all)
/api/v1/silolink/statusGETRequest SiloLink status

Launch Example

// POST /api/v1/silolink/launch
const response = await fetch('/api/v1/silolink/launch', {
  method: 'POST',
  headers: headers,
  body: JSON.stringify({
    name: 'my-feature-work',
    prompt: 'Work on the auth refactor'
  })
});

// Response:
// { success: true, conversation_id: 458, message: "Launch command sent to SiloLink" }

Requires launch, stop, or status capabilities on the Silolink resource (auto-assigned to Tenant Admin).

mdi-text-box-check Usage Patterns

CLAUDE.md Integration

Add this to your project's CLAUDE.md to instruct Claude Code to use SiloLink automatically:

## Remote Communication (SiloLink)

When the SiloLink MCP server is connected:

1. Register with `remote_register({ session_name: "<name>" })`.
   If a conversation_id is provided, pass it too.
2. ALL communication MUST go through SiloLink.
3. Send progress updates with `remote_notify()`.
4. When idle, use the poll loop:
   - Call `remote_poll()` — returns instantly
   - If `{ pending: true }`: sleep 3s, then poll again
   - If message received: process it, notify result, resume
5. Use `remote_ask()` for quick questions needing reply.

Poll Loop Pattern (Recommended)

// Register (use conversation_id if provided)
remote_register({ session_name: "portablemind", conversation_id: 123 })

// Poll loop
while (idle) {
  result = remote_poll()
  if (result.pending) { sleep(3s); continue }
  handle(result.message)
  remote_notify({ message: "Done: ..." })
}
Why remote_poll? remote_wait_for_command blocks for up to 30 minutes. If cancelled, the message can be lost. remote_poll is non-blocking — each call takes milliseconds, so messages always survive.

Channel Linking

SiloLink conversations in Harmoniq can be linked to external channels (Discord, Slack, Teams) using the Communications Hub.

Example workflow: Launch a session from the Harmoniq UI → Claude registers and enters the poll loop → link the conversation to Slack → send commands from Slack → Claude executes and reports results back.
mdi-monitor-dashboard Agent Sessions & Mission Control

Active SiloLink sessions appear in the Mission Control Agent Dashboard (/agent-dashboard) with real-time status tracking.

Session name — matches the conversation name Status — idle, working, waiting for human, error, completed Heartbeats — last_activity_at updated on every tool call Conversation link — click to navigate to the SiloLink conversation
mdi-information-outline Technical Details Authentication — HS256 JWTs signed with the tenant's shared key (24h validity, auto-refresh every 12h) Reconnection — Automatic exponential backoff (1s → 60s cap) Echo Prevention — Tracked outbound message IDs + prefix matching Session Cleanup — Idle sessions cleaned up after 1 hour, agent sessions completed on DSiloed Multi-Session — Multiple tmux sessions, one per conversation, running simultaneously Message Buffering — Pre-registration messages delivered once session registers API Timeouts — 30-second abort on all DSiloed API calls Shutdown — Kills all Claude tmux sessions, completes agent sessions on DSiloed
mdi-home Intro mdi-rocket-launch Start mdi-api API mdi-apps Apps mdi-code-tags Build {{ snackbar.text }}