Sinas Documentation
Introduction — What is Sinas?
Sinas is a platform for building AI-powered applications. It brings together multi-provider LLM agents, serverless Python functions, persistent state, database querying, file storage, and template rendering — all behind a single API with role-based access control.
What you can do with Sinas:
- Build AI agents with configurable LLM providers (OpenAI, Anthropic, Mistral, Ollama), tool calling, streaming responses, and agent-to-agent orchestration.
- Run Python functions in isolated Docker containers, triggered by agents, webhooks, cron schedules, or the API.
- Store and retrieve state across conversations with namespace-based access control.
- Query external databases (PostgreSQL, ClickHouse, Snowflake) through saved SQL templates that agents can use as tools.
- Manage files with versioning, metadata validation, and upload processing hooks.
- Render templates using Jinja2 for emails, notifications, and dynamic content.
- Define everything in YAML for GitOps workflows with idempotent apply, change detection, and dry-run.
Sinas runs as a set of Docker services: the API server, queue workers (for functions and agents), a scheduler, PostgreSQL, PgBouncer, Redis, ClickHouse (optional for request logging), and a web console.
Deployment
Prerequisites
- A VPS or server with 2+ CPU cores, 4GB+ RAM, 50GB+ storage
- A domain name pointed at the server (A record)
- An SMTP service for login emails (SendGrid, Mailgun, AWS SES, etc.)
Install
sudo curl -fsSL https://raw.githubusercontent.com/sinas-platform/sinas/main/install.sh -o /tmp/sinas-install.sh && sudo bash /tmp/sinas-install.sh
The installer will:
- Install Docker if needed
- Generate secure keys (
SECRET_KEY,ENCRYPTION_KEY,DATABASE_PASSWORD) - Prompt for your domain, admin email, and SMTP credentials
- Create
.envin/opt/sinas/ - Pull pre-built images from the container registry and start all services
- Caddy automatically provisions SSL via Let's Encrypt
All services start automatically: PostgreSQL, PgBouncer, Redis, ClickHouse, the backend API (port 8000), queue workers, the scheduler, and the web console (port 51245). Migrations run automatically on startup.
Update
cd /opt/sinas
docker compose pull
docker compose up -d
Manual development installation
For local development, see INSTALL.md.
3. Log in
- Open the console at
https://yourdomain.com:51245 - Enter your
SUPERADMIN_EMAILaddress - Check your inbox for the 6-digit OTP code
- Enter the code to receive your access token
4. Configure an LLM provider
Before agents can work, you need at least one LLM provider:
curl -X POST https://yourdomain.com/api/v1/llm-providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "openai",
"provider_type": "openai",
"api_key": "sk-...",
"default_model": "gpt-4o",
"is_default": true
}'
5. Start chatting
A default agent is created on startup. Create a chat and send a message:
# Create a chat with the default agent
curl -X POST https://yourdomain.com/agents/default/default/chats \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Send a message (use the chat_id from the response)
curl -X POST https://yourdomain.com/chats/{chat_id}/messages/stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Hello!"}'
Environment Variables
Required
| Variable | Description |
|---|---|
SECRET_KEY |
JWT signing key (auto-generated by install script) |
ENCRYPTION_KEY |
Fernet key for encrypting stored credentials (LLM API keys, DB passwords) |
DATABASE_PASSWORD |
PostgreSQL password |
SMTP_HOST |
SMTP server hostname (e.g., smtp.sendgrid.net) |
SMTP_PORT |
SMTP port (typically 587) |
SMTP_USER |
SMTP username |
SMTP_PASSWORD |
SMTP password or API key |
SMTP_DOMAIN |
Email "from" domain (e.g., example.com) |
SUPERADMIN_EMAIL |
Admin user created on first startup |
DOMAIN |
Your domain for Caddy auto-HTTPS (e.g., sinas.example.com) |
Application
| Variable | Default | Description |
|---|---|---|
DEBUG |
false |
Enable verbose logging |
CORS_ORIGINS |
(none) | Comma-separated allowed origins |
ACCESS_TOKEN_EXPIRE_MINUTES |
15 |
JWT access token lifetime |
REFRESH_TOKEN_EXPIRE_DAYS |
30 |
Refresh token lifetime |
OTP_EXPIRE_MINUTES |
10 |
OTP code validity |
OTP_MAX_ATTEMPTS |
2 |
Wrong OTP guesses before invalidation |
Rate Limiting
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_LOGIN_IP_MAX |
10 |
Max login requests per IP per window |
RATE_LIMIT_LOGIN_EMAIL_MAX |
5 |
Max login requests per email per window |
RATE_LIMIT_OTP_IP_MAX |
10 |
Max OTP verify requests per IP per window |
RATE_LIMIT_WINDOW_SECONDS |
900 |
Rate limit window (15 minutes) |
Function Execution
| Variable | Default | Description |
|---|---|---|
FUNCTION_TIMEOUT |
300 |
Max execution time in seconds |
MAX_FUNCTION_MEMORY |
512 |
Memory limit in MB |
MAX_FUNCTION_CPU |
1.0 |
CPU cores per function |
MAX_FUNCTION_STORAGE |
1g |
Disk storage limit |
FUNCTION_CONTAINER_IDLE_TIMEOUT |
3600 |
Idle container cleanup (seconds) |
ALLOW_PACKAGE_INSTALLATION |
true |
Allow pip install in functions |
ALLOWED_PACKAGES |
(all) | Comma-separated package whitelist |
Sandbox Containers
| Variable | Default | Description |
|---|---|---|
SANDBOX_MIN_SIZE |
4 |
Containers to pre-create |
SANDBOX_MAX_SIZE |
20 |
Maximum sandbox containers |
SANDBOX_MIN_IDLE |
2 |
Replenish when idle drops below this |
SANDBOX_MAX_EXECUTIONS |
100 |
Recycle after N executions |
Agent Processing
| Variable | Default | Description |
|---|---|---|
MAX_TOOL_ITERATIONS |
25 |
Max consecutive tool-call rounds per message |
MAX_HISTORY_MESSAGES |
100 |
Messages loaded for conversation context |
AGENT_JOB_TIMEOUT |
600 |
Agent job timeout (seconds) |
CODE_EXECUTION_TIMEOUT |
120 |
Code execution timeout (seconds) |
Scaling
| Variable | Default | Description |
|---|---|---|
BACKEND_REPLICAS |
1 |
Backend API replicas |
UVICORN_WORKERS |
4 |
Workers per backend replica |
QUEUE_WORKER_REPLICAS |
2 |
Function queue worker replicas |
QUEUE_AGENT_REPLICAS |
2 |
Agent queue worker replicas |
QUEUE_FUNCTION_CONCURRENCY |
10 |
Concurrent functions per worker |
QUEUE_AGENT_CONCURRENCY |
5 |
Concurrent agent jobs per worker |
Resource Limits (Docker)
| Variable | Default | Description |
|---|---|---|
APP_CPU_LIMIT |
2.0 |
Max CPU cores for backend container |
APP_MEMORY_LIMIT |
2G |
Max RAM for backend container |
APP_CPU_RESERVATION |
0.5 |
Guaranteed CPU cores |
APP_MEMORY_RESERVATION |
512M |
Guaranteed RAM |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_USER |
postgres |
PostgreSQL user |
DATABASE_HOST |
postgres |
PostgreSQL host |
DATABASE_PORT |
5432 |
PostgreSQL port |
DATABASE_NAME |
sinas |
Database name |
DATABASE_URL |
(built from above) | Full connection string (overrides individual vars) |
REDIS_URL |
redis://redis:6379/0 |
Redis connection string |
ClickHouse (Optional)
| Variable | Default | Description |
|---|---|---|
CLICKHOUSE_HOST |
clickhouse |
ClickHouse host |
CLICKHOUSE_PORT |
8123 |
ClickHouse HTTP port |
CLICKHOUSE_USER |
default |
ClickHouse user |
CLICKHOUSE_PASSWORD |
(empty) | ClickHouse password |
CLICKHOUSE_DATABASE |
sinas |
ClickHouse database |
CLICKHOUSE_RETENTION_DAYS |
90 |
Data retention (no S3) |
CLICKHOUSE_HOT_RETENTION_DAYS |
30 |
Hot retention (with S3) |
CLICKHOUSE_S3_ENDPOINT |
(none) | S3 endpoint for tiered storage |
CLICKHOUSE_S3_BUCKET |
(none) | S3 bucket name |
CLICKHOUSE_S3_ACCESS_KEY_ID |
(none) | S3 access key |
CLICKHOUSE_S3_SECRET_ACCESS_KEY |
(none) | S3 secret key |
CLICKHOUSE_S3_REGION |
us-east-1 |
S3 region |
Declarative Config
| Variable | Default | Description |
|---|---|---|
CONFIG_FILE |
(none) | Path to YAML config file |
AUTO_APPLY_CONFIG |
false |
Apply config file on startup |
The simplest useful setup requires the required variables plus a configured LLM provider. Everything else (functions, skills, state, etc.) is optional and can be added incrementally.
Concepts
Namespaces
Most resources are organized by namespace and name. A namespace groups related resources (e.g., support/ticket-agent, analytics/daily-report). The default namespace is default. Resources are uniquely identified by their namespace/name pair.
Tools
Agents interact with the outside world through tools — capabilities you enable per agent:
| Tool type | What it does |
|---|---|
| Functions | Execute Python code in isolated containers |
| Agents | Call other agents as sub-agents |
| Skills | Retrieve instruction/knowledge documents |
| Queries | Run SQL against external databases |
| Collections | Search uploaded files |
| States | Read and write persistent key-value data |
Trigger Types
Functions and agents can be triggered in multiple ways:
- API — Direct execution via the runtime API
- Manual — Via the console UI
- Agent — Called as a tool during a chat conversation
- Webhook — Via an HTTP request to a configured endpoint
- Schedule — Via a cron expression on a timer
- CDC — Automatically when rows are inserted or updated in an external database table
Declarative Configuration
All resources can be defined in a YAML file and applied idempotently via the API or on startup. Config-managed resources are tracked with checksums for change detection. See Config Manager for details.
Authentication
Sinas uses email-based OTP authentication, with API keys available for programmatic access.
OTP Authentication
- Client sends email to
POST /auth/login - Sinas sends a 6-digit code to that email (valid for 10 minutes by default)
- Client submits code to
POST /auth/verify-otp - Sinas returns an access token (short-lived JWT, default 15 min) and a refresh token (long-lived, default 30 days)
- Use the access token in the
Authorization: Bearer <token>header - When the access token expires, use
POST /auth/refreshto get a new one
Endpoints:
POST /auth/login # Send OTP to email
POST /auth/verify-otp # Verify OTP, receive tokens
POST /auth/refresh # Get new access token using refresh token
POST /auth/logout # Revoke refresh token
GET /auth/me # Get current user info
API Keys
For programmatic access (scripts, CI/CD, integrations), create API keys instead of using short-lived JWT tokens. Each key has its own set of permissions (a subset of the creating user's permissions).
POST /api/v1/api-keys # Create key (plaintext returned once)
GET /api/v1/api-keys # List keys
GET /api/v1/api-keys/{id} # Get key details
DELETE /api/v1/api-keys/{id} # Revoke key
API keys can be used via Authorization: Bearer <key> or X-API-Key: <key> headers. Keys can have optional expiration dates.
Role-Based Access Control (RBAC)
Overview
Users are assigned to roles, and roles define permissions. A user's effective permissions are the union of all permissions from all their roles (OR logic). Permissions are loaded from the database on every request — changes take effect immediately.
Default Roles
| Role | Description |
|---|---|
| Admins | Full access to everything (sinas.*:all) |
| Users | Create and manage own resources, chat with any agent, execute own functions |
| GuestUsers | Read and update own profile only |
Permission Format
<service>.<resource>[/<path>].<action>:<scope>
Components:
| Part | Description | Examples |
|---|---|---|
| Service | Top-level namespace | sinas, or a custom prefix like titan, acme |
| Resource | Resource type | agents, functions, states, users |
| Path | Optional namespace/name path | /marketing/send_email, /* |
| Action | What operation is allowed | create, read, update, delete, execute, chat |
| Scope | Ownership scope | :own (user's resources), :all (all resources) |
Permission Matching Rules
Scope hierarchy: :all automatically grants :own. A user with sinas.agents.read:all passes any check for sinas.agents.read:own.
Wildcards can be used at any level:
| Pattern | Matches |
|---|---|
sinas.*:all |
Everything in Sinas (admin access) |
sinas.agents/*/*.chat:all |
Chat with any agent in any namespace |
sinas.functions/marketing/*.execute:own |
Execute any function in the marketing namespace |
sinas.states/*.read:own |
Read own states in any namespace |
sinas.chats.*:own |
All chat actions (read, update, delete) on own chats |
Namespaced resource permissions use slashes in the resource path:
sinas.agents/support/ticket-bot.chat:own # Chat with specific agent
sinas.functions/*/send_email.execute:own # Execute send_email in any namespace
sinas.states/api_keys.read:all # Read all shared states in api_keys namespace
Non-namespaced resource permissions use simple dot notation:
sinas.webhooks.create:own # Create webhooks
sinas.schedules.read:own # Read own schedules
sinas.users.update:own # Update own profile
Custom Permissions
The permission system is not limited to sinas.*. You can define permissions with any service prefix for your own applications:
titan.student_profile.read:own
titan.courses/math/*.enroll:own
acme.billing.invoices.read:all
myapp.*:all
These work identically to built-in permissions — same wildcard matching, same scope hierarchy. This lets you use Sinas as the authorization backend for external applications.
Checking Permissions from External Services
Use the POST /auth/check-permissions endpoint to verify whether the current user (identified by their Bearer token or API key) has specific permissions:
curl -X POST https://yourdomain.com/auth/check-permissions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"permissions": ["titan.student_profile.read:own", "titan.courses.enroll:own"],
"logic": "AND"
}'
Response:
{
"result": true,
"logic": "AND",
"checks": [
{"permission": "titan.student_profile.read:own", "has_permission": true},
{"permission": "titan.courses.enroll:own", "has_permission": true}
]
}
logic: "AND"— User must have ALL listed permissions (default)logic: "OR"— User must have AT LEAST ONE of the listed permissions
This makes Sinas usable as a centralized authorization service for any number of external applications.
Managing Roles
POST /api/v1/roles # Create role
GET /api/v1/roles # List roles
GET /api/v1/roles/{name} # Get role details
PATCH /api/v1/roles/{name} # Update role
DELETE /api/v1/roles/{name} # Delete role
POST /api/v1/roles/{name}/members # Add user to role
DELETE /api/v1/roles/{name}/members/{id} # Remove user from role
POST /api/v1/roles/{name}/permissions # Set permission
DELETE /api/v1/roles/{name}/permissions # Remove permission
GET /api/v1/permissions/reference # List all known permissions
Runtime API vs Management API
Sinas has two API layers:
Runtime API (/)
The runtime API is mounted at the root. It handles authentication, chat, execution, state, file operations, and discovery. These are the endpoints your applications and end users interact with.
/auth/... # Authentication
/agents/... # Create chats with agents
/chats/... # Send messages, manage chats
/functions/... # Execute functions (sync and async)
/queries/... # Execute database queries
/webhooks/... # Trigger webhook-linked functions
/executions/... # View execution history and results
/jobs/... # Check job status
/states/... # Key-value state storage
/files/... # Upload, download, search files
/templates/... # Render and send templates
/components/... # Render components, proxy endpoints
/manifests/... # Manifest status validation
/discovery/... # List resources visible to the current user
Management API (/api/v1/)
The management API handles CRUD operations on all resources. These are typically used by admins, the console UI, and configuration tools.
/api/v1/agents/... # Agent CRUD
/api/v1/functions/... # Function CRUD
/api/v1/skills/... # Skill CRUD
/api/v1/llm-providers/... # LLM provider management (admin)
/api/v1/database-connections/... # DB connection management (admin)
/api/v1/queries/... # Query CRUD
/api/v1/collections/... # Collection CRUD
/api/v1/templates/... # Template CRUD
/api/v1/webhooks/... # Webhook CRUD
/api/v1/schedules/... # Schedule CRUD
/api/v1/components/... # Component CRUD + compilation + share links
/api/v1/manifests/... # Manifest CRUD
/api/v1/roles/... # Role & permission management
/api/v1/users/... # User management
/api/v1/api-keys/... # API key management
/api/v1/dependencies/... # Python dependency approval (admin)
/api/v1/packages/... # Integration package management
/api/v1/workers/... # Worker management (admin)
/api/v1/containers/... # Container pool management (admin)
/api/v1/config/... # Declarative config apply/validate/export (admin)
/api/v1/queue/... # Queue stats, job list, DLQ, cancel (admin)
/api/v1/request-logs/... # Request log search (admin)
OpenAI SDK Adapter (/adapters/openai)
An OpenAI SDK-compatible API that maps to Sinas agents and LLM providers. Point any OpenAI SDK client at this endpoint to use Sinas agents.
POST /adapters/openai/v1/chat/completions # Chat completion (maps to agent or direct LLM)
GET /adapters/openai/v1/models # List available models (agents + provider models)
GET /adapters/openai/v1/models/{model_id} # Get model info
Agents are listed as models with names like agent:namespace/name. Provider models are listed with their provider prefix.
Interactive API Docs
Swagger UI is available at /docs (runtime API), /api/v1/docs (management API), and /adapters/openai/docs (OpenAI adapter) for exploring all endpoints and schemas interactively.
Discovery Endpoints
The discovery API returns resources visible to the current user, optionally filtered by app context:
GET /discovery/agents # Agents the user can chat with
GET /discovery/functions # Functions the user can see
GET /discovery/skills # Skills the user can see
GET /discovery/collections # Collections the user can access
GET /discovery/templates # Templates the user can use
Pass an app context via the X-Application header or ?app=namespace/name query parameter to filter results to a specific app's exposed namespaces.
Reference
Build
Agents
Agents are configurable AI assistants. Each agent has an LLM provider, a system prompt, and a set of enabled tools.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier (e.g., support/ticket-agent) |
llm_provider_id |
LLM provider to use (null = system default) |
model |
Model override (null = provider's default) |
system_prompt |
Jinja2 template for the system message |
temperature |
Sampling temperature (default: 0.7) |
max_tokens |
Max token limit for responses |
input_schema |
JSON Schema for validating chat input variables |
output_schema |
JSON Schema for validating agent output |
initial_messages |
Few-shot example messages |
enabled_functions |
Functions available as tools (list of namespace/name) |
function_parameters |
Default parameter values per function (supports Jinja2) |
enabled_agents |
Other agents callable as sub-agents |
enabled_skills |
Skills available to the agent |
enabled_queries |
Database queries available as tools |
query_parameters |
Default query parameter values |
enabled_collections |
File collections the agent can search |
enabled_stores |
Stores the agent can access. List of {"store": "namespace/name", "access": "readonly"} or {"store": "namespace/name", "access": "readwrite"} |
enabled_connectors |
Connectors available as tools. List of {"connector": "namespace/name", "operations": [...], "parameters": {"op_name": {"param": "value"}}}. Parameters support Jinja2 templates and are locked (hidden from LLM). |
hooks |
Message lifecycle hooks. {"on_user_message": [...], "on_assistant_message": [...]} |
icon |
Icon reference (see Icons) |
Message hooks: Functions that run before/after agent messages. Each hook has:
function: reference to a function (namespace/name)async: if true, fire-and-forget (no impact on latency)on_timeout:block(stop pipeline) orpassthrough(continue) — sync hooks only
Hook functions receive {"message": {"role": "...", "content": "..."}, "chat_id": "...", "agent": {"namespace": "...", "name": "..."}, "user_id": "..."} and can return:
{"content": "..."}to mutate the message{"block": true, "reply": "..."}to stop the pipelinenullto pass through unchanged
agents:
- name: my-agent
hooks:
onUserMessage:
- function: default/guardrail
async: false
onTimeout: block
onAssistantMessage:
- function: default/pii-filter
async: false
onTimeout: passthrough
Management endpoints:
POST /api/v1/agents # Create agent
GET /api/v1/agents # List agents
GET /api/v1/agents/{namespace}/{name} # Get agent
PUT /api/v1/agents/{namespace}/{name} # Update agent
DELETE /api/v1/agents/{namespace}/{name} # Delete agent
Runtime endpoints (chats):
POST /agents/{namespace}/{name}/invoke # Invoke (sync request/response)
POST /agents/{namespace}/{name}/chats # Create chat
GET /chats # List user's chats
GET /chats/{id} # Get chat with messages
PUT /chats/{id} # Update chat
DELETE /chats/{id} # Delete chat
POST /chats/{id}/messages # Send message
POST /chats/{id}/messages/stream # Send message (SSE streaming)
GET /chats/{id}/stream/{channel_id} # Reconnect to active stream
POST /chats/{id}/approve-tool/{tool_call_id} # Approve/reject a tool call
Invoke endpoint: A synchronous request/response alternative to the two-step chat flow. Intended for integrations (Slack, Telegram, webhooks) that need a simple call-and-response.
# Simple invoke
curl -X POST https://yourdomain.com/agents/support/helper/invoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "What is the status of order #123?"}'
# → {"reply": "Your order is on its way...", "chat_id": "c_abc123"}
# With session key (conversation continuity)
curl -X POST https://yourdomain.com/agents/support/helper/invoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Follow up on that", "session_key": "slack:U09ABC123"}'
# → Same chat_id as previous call with this session_key
session_key: Maps an external identifier (Slack channel, Telegram chat, WhatsApp number) to a persistent Sinas chat. One chat per(agent_id, session_key)pair.reset: true: Archives the existing session and starts a new conversation.input: Agent input variables, only used when creating a new chat.- Streams internally, returns assembled reply as a single JSON payload.
How chat works:
- Create a chat linked to an agent (optionally with input variables validated against
input_schema) - Send a message — Sinas builds the conversation context with the system prompt, preloaded skills, message history, and available tools
- The LLM generates a response, possibly calling tools
- If tools are called, Sinas executes them (in parallel where possible) and sends results back to the LLM for a follow-up response
- The final response is streamed to the client via Server-Sent Events
Ephemeral chats can be created with a TTL by passing expires_in (seconds) when creating the chat. Expired chats are automatically hard-deleted (with all messages) by a scheduled cleanup job:
# Create an ephemeral chat that expires in 1 hour
curl -X POST https://yourdomain.com/agents/default/default/chats \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"expires_in": 3600}'
Chat archiving — Chats can be archived via PUT /chats/{id} with {"archived": true}. Archived chats are hidden from the default list but can be included with ?include_archived=true.
Agent-to-agent calls go through the Redis queue so sub-agents run in separate workers, avoiding recursive blocking. Results stream back via Redis Streams.
Function parameter defaults pre-fill values when an agent calls a function. Supports Jinja2 templates referencing the agent's input variables:
{
"email/send_email": {
"sender": "{{company_email}}",
"priority": "high"
}
}
Functions
Functions are Python code that runs in isolated Docker containers. They can be used as agent tools, triggered by webhooks or schedules, or executed directly.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
code |
Python source code |
description |
Shown to the LLM when used as an agent tool |
input_schema |
JSON Schema for input validation |
output_schema |
JSON Schema for output validation |
shared_pool |
Run in shared container instead of sandbox container (admin-only) |
requires_approval |
Require user approval when called by an agent |
timeout |
Per-function timeout in seconds (overrides global FUNCTION_TIMEOUT, default: null) |
Function signature
The entry point must be named handler. It receives two arguments — input_data (validated against input_schema) and context (execution metadata):
def handler(input_data, context):
# input_data: dict validated against input_schema
#
# context dict — always present:
# {
# "user_id": str, # ID of the user who triggered execution
# "user_email": str, # Email of the triggering user
# "access_token": str, # Short-lived JWT for calling the Sinas API
# "execution_id": str, # Unique execution ID
# "trigger_type": str, # "AGENT" | "API" | "WEBHOOK" | "SCHEDULE" | "CDC" | "HOOK" | "MANUAL"
# "chat_id": str, # Chat ID (when triggered by an agent, empty otherwise)
# "secrets": dict, # Decrypted secrets (shared pool only): {"NAME": "value"}
# }
return {"result": "value"} # Must match output_schema
Legacy: Functions named after the resource name (e.g.
def send_email(input_data, context)) still work but log a deprecation warning. Migrate todef handler(...).
The access_token lets functions call back into the Sinas API with the triggering user's identity — useful for reading state, triggering other functions, or accessing any other endpoint.
Trigger-specific input_data
Depending on how the function is invoked, input_data is populated differently:
| Trigger | input_data contents |
|---|---|
| AGENT / API / MANUAL / SCHEDULE | Values matching your input schema, provided by the caller |
| WEBHOOK | Webhook default_values merged with request body/query params |
| CDC | {"table": "schema.table", "operation": "CHANGE", "rows": [...], "poll_column": str, "count": int, "timestamp": str} |
| HOOK | {"message": {"role": "user"|"assistant", "content": "..."}, "chat_id": str, "agent": {"namespace": str, "name": str}, "session_key": str|null, "user_id": str} |
| Collection content filter | {"content_base64": str, "namespace": str, "collection": str, "filename": str, "content_type": str, "size_bytes": int, "user_metadata": dict, "user_id": str} |
| Collection post-upload | {"file_id": str, "namespace": str, "collection": str, "filename": str, "version": int, "file_path": str, "user_id": str, "metadata": dict} |
Hook function return values
Functions used as message hooks (configured in agent's hooks field) can return:
| Return value | Effect |
|---|---|
None or {} |
Pass through unchanged |
{"content": "..."} |
Mutate the message content |
{"block": true, "reply": "..."} |
Block the pipeline, return reply to client (sync hooks only) |
Async hooks run fire-and-forget. For on_assistant_message async hooks, the return value retroactively updates the stored message (not the already-streamed response).
Interactive input (shared containers only)
Functions running in shared containers (shared_pool=true) can call input() to pause and wait for user input:
def handler(input_data, context):
name = input("What is your name?") # Pauses execution
confirm = input(f"Confirm {name}?") # Can call multiple times
return {"name": name, "confirmed": confirm}
When input() is called:
- The execution status changes to
AWAITING_INPUTwith the prompt string - The function thread blocks until a resume value is provided
- The calling agent or API client resumes execution with the user's response
- Multiple
input()calls are supported (each triggers a new pause/resume cycle)
In sandbox containers (shared_pool=false), calling input() raises a RuntimeError.
Execution: Functions run in pre-warmed Docker containers from a managed pool. Input is validated before execution, output is validated after. All executions are logged with status, duration, input/output, and any errors.
Endpoints:
POST /functions/{namespace}/{name}/execute # Execute (sync, waits for result)
POST /functions/{namespace}/{name}/execute/async # Execute (async, returns execution_id)
POST /api/v1/functions # Create function
GET /api/v1/functions # List functions
GET /api/v1/functions/{namespace}/{name} # Get function
PUT /api/v1/functions/{namespace}/{name} # Update function
DELETE /api/v1/functions/{namespace}/{name} # Delete function
GET /api/v1/functions/{namespace}/{name}/versions # List code versions
Secrets
Write-only credential store. Values are encrypted at rest and never returned via the API. Secrets are available in function context as context['secrets'] — only for shared pool (trusted) functions. Connectors resolve auth from secrets automatically.
Visibility:
shared(default) — global, available to all users and connectorsprivate— per-user, only used when that user triggers a connector or function
Private secrets override shared secrets with the same name. This enables multi-tenant patterns: admin sets a shared HUBSPOT_API_KEY, individual users can override with their own private key.
Endpoints:
POST /api/v1/secrets # Create or update (upsert by name+visibility)
GET /api/v1/secrets # List names and descriptions (no values)
GET /api/v1/secrets/{name} # Get metadata (no value)
PUT /api/v1/secrets/{name} # Update value or description
DELETE /api/v1/secrets/{name} # Delete
Access at runtime (shared pool functions only):
def my_function(input, context):
# Private secrets override shared for the calling user
token = context['secrets']['SLACK_BOT_TOKEN']
YAML config:
secrets:
- name: SLACK_BOT_TOKEN
value: xoxb-... # omit to skip value update on re-apply
description: Slack bot OAuth token
# visibility defaults to "shared" in YAML config
Connectors
Named HTTP client configurations with typed operations. Executed in-process in the backend (no container overhead). Operations are exposed as agent tools. Auth resolved from the Secrets store at call time.
Endpoints:
POST /api/v1/connectors # Create connector
GET /api/v1/connectors # List connectors
GET /api/v1/connectors/{namespace}/{name} # Get connector
PUT /api/v1/connectors/{namespace}/{name} # Update connector
DELETE /api/v1/connectors/{namespace}/{name} # Delete connector
POST /api/v1/connectors/parse-openapi # Parse OpenAPI spec (no connector required)
POST /api/v1/connectors/{namespace}/{name}/import-openapi # Import operations from OpenAPI spec into connector
POST /api/v1/connectors/{namespace}/{name}/test/{operation} # Test an operation
Auth types: bearer, basic, api_key, sinas_token (forwards caller's JWT), none
Auth is resolved from the Secrets store. Private secrets override shared for the calling user — enabling multi-tenant patterns where each user can have their own API key for the same connector.
Agent configuration:
agents:
- name: slack-bot
enabledConnectors:
- connector: default/slack-api
operations: [post_message, get_channel_info]
parameters:
post_message:
channel: "{{ default_channel }}"
YAML config:
connectors:
- name: slack-api
namespace: default
baseUrl: https://slack.com/api
auth:
type: bearer
secret: SLACK_BOT_TOKEN
operations:
- name: post_message
method: POST
path: /chat.postMessage
parameters:
type: object
properties:
channel: { type: string }
text: { type: string }
required: [channel, text]
Execution history:
GET /executions # List executions (filterable by chat_id, trigger_type, status)
GET /executions/{execution_id} # Get execution details (includes tool_call_id link)
POST /executions/{execution_id}/continue # Resume a paused execution with user input
Executions include a tool_call_id field linking them to the tool call that triggered them, enabling execution tree visualization in the Logs page.
Input schema presets: The function editor includes built-in presets for common input/output schemas. Use the "Load preset" dropdown when editing schemas:
| Preset | Use case |
|---|---|
| Pre-upload filter | Content filtering before file upload (receives file content, returns approved/rejected) |
| Post-upload | Processing after successful file upload (receives file_id, metadata) |
| CDC (Change Data Capture) | Processing database changes (receives table, rows, poll_column, count) |
| Message Hook | Message lifecycle hook (receives message, chat_id, agent; returns content mutation or block) |
Components
Components are embeddable UI widgets built with JSX/HTML/JS and compiled by Sinas into browser-ready bundles. They can call agents, functions, queries, and access state through proxy endpoints.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
title |
Display title |
source_code |
JSX/HTML/JS source |
compiled_bundle |
Auto-generated browser-ready JS |
input_schema |
JSON Schema for component configuration |
enabled_agents |
Agents the component can call |
enabled_functions |
Functions the component can call |
enabled_queries |
Queries the component can execute |
enabled_components |
Other components it can embed |
enabled_stores |
Stores the component can access ({"store": "ns/name", "access": "readonly|readwrite"}) |
css_overrides |
Custom CSS |
visibility |
private, shared, or public |
Components use the sinas-ui library (loaded from npm/unpkg) for a consistent look and feel.
Management endpoints:
POST /api/v1/components # Create component
GET /api/v1/components # List components
GET /api/v1/components/{namespace}/{name} # Get component
PUT /api/v1/components/{namespace}/{name} # Update component
DELETE /api/v1/components/{namespace}/{name} # Delete component
POST /api/v1/components/{namespace}/{name}/compile # Trigger compilation
Share links allow embedding components outside Sinas with optional expiration and view limits:
POST /api/v1/components/{namespace}/{name}/shares # Create share link
GET /api/v1/components/{namespace}/{name}/shares # List share links
DELETE /api/v1/components/{namespace}/{name}/shares/{token} # Revoke share link
Runtime rendering:
GET /components/{namespace}/{name}/render # Render as full HTML page
GET /components/shared/{token} # Render via share token
Proxy endpoints allow components to call backend resources from the browser securely — the proxy enforces the component's enabled_* permissions:
POST /components/{ns}/{name}/proxy/queries/{q_ns}/{q_name}/execute # Execute query
POST /components/{ns}/{name}/proxy/functions/{fn_ns}/{fn_name}/execute # Execute function
POST /components/{ns}/{name}/proxy/states/{state_ns} # Access state
Queries
Queries are saved SQL templates that can be executed directly or used as agent tools.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
database_connection_id |
Which database connection to use |
description |
Shown to the LLM as the tool description |
operation |
read or write |
sql |
SQL with :param_name placeholders |
input_schema |
JSON Schema for parameter validation |
output_schema |
JSON Schema for output validation |
timeout_ms |
Query timeout (default: 5000ms) |
max_rows |
Max rows returned for read operations (default: 1000) |
Agent query parameters support defaults and locking:
query_parameters:
"analytics/user_orders":
"user_id":
value: "{{user_id}}" # Jinja2 template from agent input
locked: true # Hidden from LLM, always injected
"status":
value: "pending"
locked: false # Shown to LLM with default, LLM can override
Locked parameters prevent the LLM from seeing or modifying security-sensitive values (like user_id).
Contextual parameters: The following parameters are automatically injected into every query execution and can be referenced in SQL:
| Parameter | Description |
|---|---|
:user_id |
UUID of the user who triggered the query |
:user_email |
Email of the triggering user |
These are always available regardless of the query's input_schema. Use them for row-level security:
-- Only return orders belonging to the calling user
SELECT * FROM orders WHERE created_by = :user_id
-- Audit trail
INSERT INTO audit_log (action, performed_by) VALUES (:action, :user_email)
Endpoints:
POST /queries/{namespace}/{name}/execute # Execute with parameters (runtime)
POST /api/v1/queries # Create query
GET /api/v1/queries # List queries
GET /api/v1/queries/{namespace}/{name} # Get query
PUT /api/v1/queries/{namespace}/{name} # Update query
DELETE /api/v1/queries/{namespace}/{name} # Delete query
Configure
Skills
Skills are reusable instruction documents that give agents specialized knowledge or guidelines.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
description |
What the skill helps with (shown to the LLM as the tool description) |
content |
Markdown instructions |
Two modes:
| Mode | Behavior | Best for |
|---|---|---|
Preloaded (preload: true) |
Injected into the system prompt | Tone guidelines, safety rules, persona traits |
Progressive (preload: false) |
Exposed as a tool the LLM calls when needed | Research methods, domain expertise, task-specific instructions |
Example agent configuration:
enabled_skills:
- skill: "default/tone_guidelines"
preload: true # Always present in system prompt
- skill: "default/web_research"
preload: false # LLM decides when to retrieve it
Endpoints:
POST /api/v1/skills # Create skill
GET /api/v1/skills # List skills
GET /api/v1/skills/{namespace}/{name} # Get skill
PUT /api/v1/skills/{namespace}/{name} # Update skill
DELETE /api/v1/skills/{namespace}/{name} # Delete skill
LLM Providers
LLM providers connect Sinas to language model APIs.
Supported providers:
| Type | Description |
|---|---|
openai |
OpenAI API (GPT-4, GPT-4o, o1, etc.) and OpenAI-compatible endpoints |
anthropic |
Anthropic API (Claude 3, Claude 4, etc.) |
mistral |
Mistral AI (Mistral Large, Pixtral, etc.) |
ollama |
Local models via Ollama |
Key properties:
| Property | Description |
|---|---|
name |
Unique provider name |
provider_type |
openai, anthropic, mistral, or ollama |
api_key |
API key (encrypted at rest, never returned in API responses) |
api_endpoint |
Custom endpoint URL (required for Ollama, useful for proxies) |
default_model |
Model used when agents don't specify one |
config |
Additional settings (e.g., max_tokens, organization_id) |
is_default |
Whether this is the system-wide default provider |
Provider resolution for agents:
- Agent's explicit
llm_provider_idif set - Agent's
modelfield with the resolved provider - Provider's
default_model - System default provider as final fallback
Endpoints (admin only):
POST /api/v1/llm-providers # Create provider
GET /api/v1/llm-providers # List providers
GET /api/v1/llm-providers/{name} # Get provider
PATCH /api/v1/llm-providers/{id} # Update provider
DELETE /api/v1/llm-providers/{id} # Delete provider
Database Connections
Database connections store credentials and manage connection pools for external databases.
Supported databases: PostgreSQL, ClickHouse, Snowflake
Key properties:
| Property | Description |
|---|---|
name |
Unique connection name |
connection_type |
postgresql, clickhouse, or snowflake |
host, port, database, username, password |
Connection details |
ssl_mode |
Optional SSL configuration |
config |
Pool settings (min_pool_size, max_pool_size) |
Passwords are encrypted at rest. Connection pools are managed automatically and invalidated when settings change.
Endpoints (admin only):
POST /api/v1/database-connections # Create connection
GET /api/v1/database-connections # List connections
GET /api/v1/database-connections/{name} # Get by name
PATCH /api/v1/database-connections/{id} # Update
DELETE /api/v1/database-connections/{id} # Delete
POST /api/v1/database-connections/test # Test raw connection params
POST /api/v1/database-connections/{id}/test # Test saved connection
Templates
Templates are Jinja2-based documents for emails, notifications, and dynamic content.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
title |
Optional title template (e.g., email subject) |
html_content |
Jinja2 HTML template |
text_content |
Optional plain-text fallback |
variable_schema |
JSON Schema for validating template variables |
HTML output is auto-escaped to prevent XSS. Missing variables cause errors (strict mode).
Management endpoints:
POST /api/v1/templates # Create template
GET /api/v1/templates # List templates
GET /api/v1/templates/{id} # Get by ID
GET /api/v1/templates/by-name/{namespace}/{name} # Get by name
PATCH /api/v1/templates/{id} # Update
DELETE /api/v1/templates/{id} # Delete
Runtime endpoints:
POST /templates/{id}/render # Render template with variables
POST /templates/{id}/send # Render and send as email
Triggers
Webhooks
Webhooks expose functions as HTTP endpoints. When a request arrives at a webhook path, Sinas executes the linked function with the request data.
Key properties:
| Property | Description |
|---|---|
path |
URL path (e.g., stripe/payment-webhook) |
http_method |
GET, POST, PUT, DELETE, or PATCH |
function_namespace / function_name |
Target function |
requires_auth |
Whether the caller must provide a Bearer token |
default_values |
Default parameters merged with request data (request takes priority) |
How input is extracted:
POST/PUT/PATCHwith JSON body → body becomes the inputGET→ query parameters become the input- Default values are merged underneath (request data overrides)
Example:
# Create a webhook
curl -X POST https://yourdomain.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"path": "stripe/payment",
"function_namespace": "payments",
"function_name": "process_webhook",
"http_method": "POST",
"requires_auth": false,
"default_values": {"source": "stripe"}
}'
# Trigger it
curl -X POST https://yourdomain.com/webhooks/stripe/payment \
-H "Content-Type: application/json" \
-d '{"event": "charge.succeeded", "amount": 1000}'
# Function receives: {"source": "stripe", "event": "charge.succeeded", "amount": 1000}
Endpoints:
POST /api/v1/webhooks # Create webhook
GET /api/v1/webhooks # List webhooks
GET /api/v1/webhooks/{path} # Get webhook
PATCH /api/v1/webhooks/{path} # Update webhook
DELETE /api/v1/webhooks/{path} # Delete webhook
Schedules
Schedules trigger functions or agents on a cron timer.
Key properties:
| Property | Description |
|---|---|
name |
Unique name (per user) |
schedule_type |
function or agent |
target_namespace / target_name |
Function or agent to trigger |
cron_expression |
Standard cron expression (e.g., 0 9 * * MON-FRI) |
timezone |
Schedule timezone (default: UTC) |
input_data |
Input passed to the function or agent |
content |
Message content (agent schedules only) |
For agent schedules, a new chat is created for each run with the schedule name and timestamp as the title.
Endpoints:
POST /api/v1/schedules # Create schedule
GET /api/v1/schedules # List schedules
GET /api/v1/schedules/{name} # Get schedule
PATCH /api/v1/schedules/{name} # Update schedule
DELETE /api/v1/schedules/{name} # Delete schedule
Database Triggers (CDC)
Database triggers watch external database tables for changes and automatically execute functions when new or updated rows are detected. This is poll-based Change Data Capture — no setup required on the source database.
How it works:
- A separate CDC service polls the configured table at a fixed interval
- It queries rows where
poll_column > last_bookmark(e.g.,updated_at > '2026-03-01T10:00:00') - If new rows are found, they are batched into a single function call
- The bookmark advances to the highest value in the batch
- On first activation, the bookmark is set to
MAX(poll_column)— no backfill of existing data
Key properties:
| Property | Description |
|---|---|
name |
Unique trigger name (per user) |
database_connection_id |
Which database connection to poll |
schema_name |
Database schema (default: public) |
table_name |
Table to watch |
operations |
["INSERT"], ["UPDATE"], or both |
function_namespace / function_name |
Function to execute when changes are detected |
poll_column |
Monotonically increasing column used as bookmark (e.g., updated_at, id) |
poll_interval_seconds |
How often to poll (1–3600, default: 10) |
batch_size |
Max rows per poll (1–10000, default: 100) |
is_active |
Enable/disable without deleting |
last_poll_value |
Current bookmark (managed automatically) |
error_message |
Last error, if any (visible in UI) |
The poll_column must be a column whose value only increases — timestamps, auto-increment IDs, or sequences. The column type is detected automatically and comparisons are cast to the correct type.
Function input payload:
When changes are detected, the target function receives all new rows in a single call:
{
"table": "public.orders",
"operation": "CHANGE",
"rows": [
{"id": 123, "status": "paid", "amount": 99.50, "updated_at": "2026-03-02T10:30:00Z"},
{"id": 124, "status": "pending", "amount": 45.00, "updated_at": "2026-03-02T10:30:01Z"}
],
"poll_column": "updated_at",
"count": 2,
"timestamp": "2026-03-02T10:30:05Z"
}
If a poll returns zero rows, no function call is made.
Error handling: On failure, the trigger logs the error to error_message and retries with exponential backoff (up to 60 seconds). The trigger continues retrying until deactivated or the issue is resolved.
Limitations:
- Cannot detect
DELETEoperations (poll-based limitation) - Changes are detected with a delay equal to the poll interval
- The
poll_columnmust never decrease — resetting it will cause missed or duplicate rows
Endpoints:
POST /api/v1/database-triggers # Create trigger
GET /api/v1/database-triggers # List triggers
GET /api/v1/database-triggers/{name} # Get trigger
PATCH /api/v1/database-triggers/{name} # Update trigger
DELETE /api/v1/database-triggers/{name} # Delete trigger
Declarative configuration:
databaseTriggers:
- name: "customer_changes"
connectionName: "prod_database"
tableName: "customers"
operations: ["INSERT", "UPDATE"]
functionName: "sync/process_customer"
pollColumn: "updated_at"
pollIntervalSeconds: 10
batchSize: 100
Storage
Collections & Files
Collections are containers for file uploads with versioning, metadata validation, and processing hooks.
Collection properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
metadata_schema |
JSON Schema that file metadata must conform to |
content_filter_function |
Function that runs on upload to approve/reject files |
post_upload_function |
Function that runs after upload for processing |
max_file_size_mb |
Per-file size limit (default: 100 MB) |
max_total_size_gb |
Total collection size limit (default: 10 GB) |
File features:
- Versioning — Every upload creates a new version. Previous versions are preserved.
- Metadata — Each file carries JSON metadata validated against the collection's schema.
- Visibility — Files can be
private(owner only) orshared(users with collection:allaccess). - Content filtering — Optional function runs on upload that can approve, reject, or modify the file.
Management endpoints:
POST /api/v1/collections # Create collection
GET /api/v1/collections # List collections
GET /api/v1/collections/{namespace}/{name} # Get collection
PUT /api/v1/collections/{namespace}/{name} # Update
DELETE /api/v1/collections/{namespace}/{name} # Delete (cascades to files)
Runtime file endpoints:
POST /files/{namespace}/{collection} # Upload file
GET /files/{namespace}/{collection} # List files
GET /files/{namespace}/{collection}/{filename} # Download file
PATCH /files/{namespace}/{collection}/{filename} # Update metadata
DELETE /files/{namespace}/{collection}/{filename} # Delete file
POST /files/{namespace}/{collection}/{filename}/url # Generate temporary download URL
POST /files/{namespace}/{collection}/search # Search files
States
States are a persistent key-value store organized by namespace. Agents use states to maintain memory and context across conversations.
Key properties:
| Property | Description |
|---|---|
namespace |
Organizational grouping (e.g., preferences, memory, api_keys) |
key |
Unique key within user + namespace |
value |
Any JSON data |
visibility |
private (owner only) or shared (users with namespace :all permission) |
description |
Optional description |
tags |
Tags for filtering and search |
relevance_score |
Priority for context retrieval (0.0–1.0, default: 1.0) |
encrypted |
If true, value is encrypted at rest with Fernet and decrypted on read |
expires_at |
Optional expiration time |
Encrypted states: Set encrypted: true when creating or updating a state to store the value encrypted. The plaintext value is stored in encrypted_value (Fernet-encrypted) while value is set to {}. On read, the value is transparently decrypted. This is useful for storing API keys, tokens, or other secrets.
Agent state access is declared per agent via enabled_stores:
enabledStores:
- store: "shared_knowledge/main"
access: readonly
- store: "conversation_memory/main"
access: readwrite
Read-only stores give the agent a retrieve_context tool. Read-write stores additionally provide save_context, update_context, and delete_context.
Endpoints:
POST /states # Create state entry
GET /states # List (supports namespace, visibility, tags, search filters)
GET /states/{id} # Get state
PUT /states/{id} # Update state
DELETE /states/{id} # Delete state
Admin
Integration Packages
Integration packages bundle agents, functions, skills, components, templates, and other resources into a shareable YAML file that can be installed with one click.
How packages work:
- Create: Select resources from your Sinas instance → export as
SinasPackageYAML - Share: Distribute the YAML file (GitHub, email, package registry)
- Install: Paste/upload the YAML → preview changes → confirm install
- Uninstall: Removes all resources created by the package in one operation
Package YAML format:
apiVersion: sinas.co/v1
kind: SinasPackage
package:
name: crm-integration
version: "1.0.0"
description: "CRM support agents and functions"
author: "team@company.com"
url: "https://github.com/company/sinas-crm"
spec:
agents: [...]
functions: [...]
skills: [...]
components: [...]
templates: [...]
queries: [...]
collections: [...]
webhooks: [...]
schedules: [...]
manifests: [...]
Key behaviors:
- Resources created by packages are tagged with
managed_by: "pkg:<name>" - Detach-on-edit: Editing a package-managed resource clears
managed_by— the resource survives uninstall - Uninstall: Deletes all resources where
managed_by = "pkg:<name>"+ the package record - Excluded types: Packages cannot include roles, users, LLM providers, or database connections (these are environment-specific)
Endpoints:
POST /api/v1/packages/install # Install package from YAML
POST /api/v1/packages/preview # Preview install (dry run)
POST /api/v1/packages/create # Create package YAML from selected resources
GET /api/v1/packages # List installed packages
GET /api/v1/packages/{name} # Get package details
DELETE /api/v1/packages/{name} # Uninstall package
GET /api/v1/packages/{name}/export # Export original YAML
Creating a package from existing resources:
curl -X POST https://yourdomain.com/api/v1/packages/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-package",
"version": "1.0.0",
"description": "My integration package",
"resources": [
{"type": "agent", "namespace": "support", "name": "ticket-bot"},
{"type": "function", "namespace": "support", "name": "lookup-customer"},
{"type": "template", "namespace": "support", "name": "ticket-reply"},
{"type": "schedule", "namespace": "default", "name": "daily-digest"}
]
}'
Supported resource types: agent, function, skill, manifest, component, query, collection, template, webhook, schedule.
Manifests
Manifests are application declarations that describe what resources, permissions, and stores an application built on Sinas requires. They enable a single API call to validate whether a user has everything needed to run the application.
Key properties:
| Property | Description |
|---|---|
namespace / name |
Unique identifier |
description |
What this manifest declares |
required_resources |
Resource references: [{"type": "agent", "namespace": "...", "name": "..."}] |
required_permissions |
Permissions the application needs |
optional_permissions |
Optional permissions for extended features |
exposed_namespaces |
Namespace filter per resource type (e.g., {"agents": ["support"]}) |
store_dependencies |
Stores the application expects: [{"store": "ns/name", "key": "optional_key"}] |
Endpoints:
POST /api/v1/manifests # Create manifest
GET /api/v1/manifests # List manifests
GET /api/v1/manifests/{namespace}/{name} # Get manifest
PUT /api/v1/manifests/{namespace}/{name} # Update
DELETE /api/v1/manifests/{namespace}/{name} # Delete
Runtime status validation:
GET /api/runtime/manifests/{namespace}/{name}/status # Validate dependencies
Returns ready: true/false with details on satisfied/missing resources, granted/missing permissions, and existing/missing store dependencies.
Users & Roles
Users are identified by email. They can be created automatically on first login (OTP or OIDC) or provisioned via the management API or declarative config.
Roles group permissions together. Users can belong to multiple roles. All permissions across all of a user's roles are combined.
User endpoints:
GET /api/v1/users # List users (admin)
POST /api/v1/users # Create user (admin)
GET /api/v1/users/{id} # Get user
PATCH /api/v1/users/{id} # Update user
DELETE /api/v1/users/{id} # Delete user (admin)
Role endpoints: See Managing Roles.
Permissions
See Role-Based Access Control (RBAC) for the full permission system documentation, including format, matching rules, custom permissions, and the check-permissions endpoint.
Quick reference of action verbs:
| Verb | Usage |
|---|---|
create |
Create a resource |
read |
View/list a resource |
update |
Modify a resource |
delete |
Remove a resource |
execute |
Run a function or query |
chat |
Chat with an agent |
render |
Render a template |
send |
Send a rendered template |
upload |
Upload a file |
download |
Download a file |
install |
Approve a package |
System Workers & Containers
Sinas has a dual-execution model for functions, plus dedicated queue workers for async job processing.
Sandbox Containers
The sandbox container pool is a set of pre-warmed, generic Docker containers for executing untrusted user code. This is the default execution mode for all functions (shared_pool=false).
How it works:
- On startup, the pool creates
sandbox_min_sizecontainers (default: 4) ready to accept work. - When a function executes, a container is acquired from the idle pool, used, and returned.
- Containers are recycled (destroyed and replaced) after
sandbox_max_executionsuses (default: 100) to prevent state leakage between executions. - If a container errors during execution, it's marked as tainted and destroyed immediately.
- A background replenishment loop monitors the idle count and creates new containers whenever it drops below
sandbox_min_idle(default: 2), up tosandbox_max_size(default: 20). - Health checks run every 60 seconds to detect and replace dead containers.
Isolation guarantees:
Each container runs with strict resource limits and security hardening:
| Constraint | Default |
|---|---|
| Memory | 512 MB (MAX_FUNCTION_MEMORY) |
| CPU | 1.0 cores (MAX_FUNCTION_CPU) |
| Disk | 1 GB (MAX_FUNCTION_STORAGE) |
| Execution time | 300 seconds (FUNCTION_TIMEOUT) |
| Temp storage | 100 MB tmpfs at /tmp |
| Capabilities | All dropped, only CHOWN/SETUID/SETGID added |
| Privilege escalation | Disabled (no-new-privileges) |
Runtime scaling:
The pool can be scaled up or down at runtime without restarting the application:
# Check current pool state
GET /api/v1/containers/stats
# → {"idle": 2, "in_use": 3, "total": 5, "max_size": 20, ...}
# Scale up for high load
POST /api/v1/containers/scale
{"target": 15}
# → {"action": "scale_up", "previous": 5, "current": 15, "added": 10}
# Scale back down (only removes idle containers — never interrupts running executions)
POST /api/v1/containers/scale
{"target": 4}
# → {"action": "scale_down", "previous": 15, "current": 4, "removed": 11}
Package installation:
When new packages are approved, existing containers don't have them yet. Use the reload endpoint to install approved packages into all idle containers:
POST /api/v1/containers/reload
# → {"status": "completed", "idle_containers": 4, "success": 4, "failed": 0}
Containers that are currently executing are unaffected. New containers created by the replenishment loop automatically include all approved packages.
Shared Containers
Functions marked shared_pool=true run in persistent shared containers instead of sandbox containers. This is an admin-only option for trusted code that benefits from longer-lived containers.
Differences from sandbox:
| Sandbox Containers | Shared Containers | |
|---|---|---|
| Trust level | Untrusted user code | Trusted admin code only |
| Isolation | Per-request (recycled after N uses) | Shared (persistent containers) |
| Lifecycle | Created/destroyed automatically | Persist until explicitly scaled down |
| Scaling | Auto-replenishment + manual | Manual via API only |
| Load balancing | First available idle container | Round-robin across workers |
| Best for | User-submitted functions | Admin functions, long-startup libraries |
When to use shared_pool=true (shared containers):
- Functions created and maintained by admins (not user-submitted code)
- Functions that import heavy libraries (pandas, scikit-learn) where container startup cost matters
- Performance-critical functions that benefit from warm containers
Management:
# List workers
GET /api/v1/workers
# Check count
GET /api/v1/workers/count
# → {"count": 4}
# Scale workers
POST /api/v1/workers/scale
{"target_count": 6}
# → {"action": "scale_up", "previous_count": 4, "current_count": 6, "added": 2}
# Reload packages in all workers
POST /api/v1/workers/reload
# → {"status": "completed", "total_workers": 6, "success": 6, "failed": 0}
Queue Workers
All function and agent executions are processed asynchronously through Redis-based queues (arq). Two separate worker types handle different workloads:
| Worker | Docker service | Queue | Concurrency | Retries |
|---|---|---|---|---|
| Function workers | queue-worker |
sinas:queue:functions |
10 jobs/worker | Up to 3 |
| Agent workers | queue-agent |
sinas:queue:agents |
5 jobs/worker | None (not idempotent) |
Function workers dequeue function execution jobs, route them to either sandbox or shared containers, track results in Redis, and handle retries. Failed jobs that exhaust retries are moved to a dead letter queue (DLQ) for inspection and manual retry.
Agent workers handle chat message processing — they call the LLM, execute tool calls, and stream responses back via Redis Streams. Agent jobs don't retry because LLM calls with tool execution have side effects.
Scaling is controlled via Docker Compose replicas:
# docker-compose.yml
queue-worker:
command: python -m arq app.queue.worker.WorkerSettings
deploy:
replicas: ${QUEUE_WORKER_REPLICAS:-2}
queue-agent:
command: python -m arq app.queue.worker.AgentWorkerSettings
deploy:
replicas: ${QUEUE_AGENT_REPLICAS:-2}
Each worker sends a heartbeat to Redis every 10 seconds (TTL: 30 seconds). If a worker dies, its heartbeat key auto-expires, making it easy to detect dead workers.
Job status tracking:
# Check job status
GET /jobs/{job_id}
# → {"status": "completed", "execution_id": "...", ...}
# Get job result
GET /jobs/{job_id}/result
# → {function output}
Jobs go through states: queued → running → completed or failed. Stale or orphaned jobs can be cancelled via the admin API:
# Cancel a running or queued job
POST /api/v1/queue/jobs/{job_id}/cancel
# → {"status": "cancelled", "job_id": "..."}
Cancellation updates the Redis status to cancelled and marks the DB execution record as CANCELLED. It also publishes to the done channel so any waiters unblock. This is a soft cancel — it does not kill running containers.
Results are stored in Redis with a 24-hour TTL.
System Endpoints
Admin endpoints for monitoring and managing the Sinas deployment. All require sinas.system.read:all or sinas.system.update:all permissions.
Health check:
GET /api/v1/system/health
Returns a comprehensive health report:
services— All Docker Compose containers with status, health, uptime, CPU %, and memory usage. Infrastructure containers (redis, postgres, pgbouncer) are listed first, followed by application containers sorted alphabetically. Sandbox and shared worker containers are included.host— Host-level CPU, memory, and disk usage (read from/procon Linux).warnings— Auto-generated alerts at three levels:critical— No queue workers running, or infrastructure services (redis, postgres, pgbouncer) downwarning— Non-infrastructure services down, unhealthy containers, DLQ items, queue backlog >50, disk/memory >90%info— Disk/memory >75%
Container restart:
POST /api/v1/system/containers/{container_name}/restart
# → {"status": "restarted", "container": "sinas-backend"}
Restarts any Docker container by name (15-second timeout). Returns 404 if the container doesn't exist.
Flush stuck jobs:
POST /api/v1/system/flush-stuck-jobs
Cancels all jobs that have been stuck in running state for over 2 hours. Useful for recovering from worker crashes or orphaned jobs.
Dependencies (Python Packages)
Functions can only use Python packages that have been approved by an admin. This prevents untrusted code from installing arbitrary dependencies.
Approval flow:
- Admin approves a dependency (optionally pinning a version)
- Package becomes available in newly created containers and workers
- Use
POST /containers/reloadorPOST /workers/reloadto install into existing containers
POST /api/v1/dependencies # Approve dependency (admin)
GET /api/v1/dependencies # List approved dependencies
DELETE /api/v1/dependencies/{id} # Remove approval (admin)
Optionally restrict which packages can be approved with a whitelist:
# In .env — only these packages can be approved
ALLOWED_PACKAGES=requests,pandas,numpy,redis,boto3
Configuration Reference
Container pool:
| Variable | Default | Description |
|---|---|---|
POOL_MIN_SIZE |
4 | Containers created on startup |
POOL_MAX_SIZE |
20 | Maximum total containers |
POOL_MIN_IDLE |
2 | Replenish when idle count drops below this |
POOL_MAX_EXECUTIONS |
100 | Recycle container after this many uses |
POOL_ACQUIRE_TIMEOUT |
30 | Seconds to wait for an available container |
Function execution:
| Variable | Default | Description |
|---|---|---|
FUNCTION_TIMEOUT |
300 | Max execution time in seconds |
MAX_FUNCTION_MEMORY |
512 | Memory limit per container (MB) |
MAX_FUNCTION_CPU |
1.0 | CPU cores per container |
MAX_FUNCTION_STORAGE |
1g | Disk storage limit |
FUNCTION_CONTAINER_IDLE_TIMEOUT |
3600 | Idle container cleanup (seconds) |
Workers and queues:
| Variable | Default | Description |
|---|---|---|
DEFAULT_WORKER_COUNT |
4 | Shared workers created on startup |
QUEUE_WORKER_REPLICAS |
2 | Function queue worker processes |
QUEUE_AGENT_REPLICAS |
2 | Agent queue worker processes |
QUEUE_FUNCTION_CONCURRENCY |
10 | Concurrent jobs per function worker |
QUEUE_AGENT_CONCURRENCY |
5 | Concurrent jobs per agent worker |
QUEUE_MAX_RETRIES |
3 | Retry attempts before DLQ |
QUEUE_RETRY_DELAY |
10 | Seconds between retries |
Packages:
| Variable | Default | Description |
|---|---|---|
ALLOW_PACKAGE_INSTALLATION |
true | Enable pip in containers |
ALLOWED_PACKAGES |
(empty) | Comma-separated whitelist (empty = all allowed) |
Icons
Agents and functions support configurable icons via the icon field. Two formats are supported:
| Format | Example | Description |
|---|---|---|
url:<url> |
url:https://example.com/icon.png |
Direct URL to an image |
collection:<ns>/<coll>/<file> |
collection:assets/icons/bot.png |
File stored in a Sinas collection |
Collection-based icons generate signed JWT URLs for private files and direct URLs for public collection files. Icons are resolved at read time via the icon resolver service.
Config Manager
The config manager supports GitOps-style declarative configuration. Define all your resources in a YAML file and apply it idempotently.
YAML structure:
apiVersion: sinas.co/v1
kind: SinasConfig
metadata:
name: my-config
description: Production configuration
spec:
roles: # Roles and permissions
users: # User provisioning
llmProviders: # LLM provider connections
databaseConnections: # External database credentials
dependencies: # Python packages (pip)
secrets: # Encrypted credentials (values omitted on export)
connectors: # HTTP connectors with typed operations
skills: # Instruction documents
components: # UI components
functions: # Python functions
queries: # Saved SQL templates
collections: # File storage collections
templates: # Jinja2 templates
stores: # State store definitions
manifests: # Application manifests
agents: # AI agent configurations
webhooks: # HTTP triggers for functions
schedules: # Cron-based triggers
databaseTriggers: # CDC polling triggers
All sections are optional — include only what you need.
Key behaviors:
- Idempotent — Applying the same config twice does nothing. Unchanged resources are skipped (SHA256 checksum comparison).
- Config-managed tracking — Resources created via config are tagged with
managed_by: "config". The system won't overwrite resources that were created manually (it warns instead). - Environment variable interpolation — Use
${VAR_NAME}in values (e.g.,apiKey: "${OPENAI_API_KEY}"). - Reference validation — Cross-references (e.g., an agent referencing a function) are validated before applying.
- Dry run — Set
dryRun: trueto preview changes without applying.
Endpoints (admin only):
POST /api/v1/config/validate # Validate YAML syntax and references
POST /api/v1/config/apply # Apply config (supports dryRun and force flags)
GET /api/v1/config/export # Export current configuration as YAML
Auto-apply on startup:
# In .env
CONFIG_FILE=config/production.yaml
AUTO_APPLY_CONFIG=true
Apply via API:
curl -X POST https://yourdomain.com/api/v1/config/apply \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"config\": \"$(cat config.yaml)\", \"dryRun\": false}"