# Documentation (/) Welcome to Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Chat, or programmatically with the API — connected to the integration catalog and every major LLM. ## Quick Start [#quick-start] Learn what you can build with Sim Build your first agent in 10 minutes Learn about the building blocks Explore the integration catalog ## Core Concepts [#core-concepts] Understand how data flows between blocks Work with workflow and environment variables Inspect workflow runs and diagnose failures Start agents via API, webhooks, or schedules ## Advanced Features [#advanced-features] Set up workspace roles and permissions Connect external services with Model Context Protocol Integrate Sim into your applications --- # What is Sim? (/academy) Sim is a unified workspace for **building and operating AI systems**. Everything you make lives in one place, and everything connects. ## The workspace [#the-workspace] A **workspace** is a collection of shared resources and the workflows that use them. * **Resources** are your data: [tables](/academy/tables/intro) (structured records), [knowledge bases](/academy/knowledge-bases/intro) (searchable memory), and [files](/academy/files/intro) (documents and media). * **Workflows** are your processes: the agents and automations that read those resources, act, and write results back. Workflows can use workspace resources they have access to: read a table, search a knowledge base, or produce a file for another workflow. ## Integrations connect you to the outside world [#integrations-connect-you-to-the-outside-world] **Integrations** are plugins. They let your resources and workflows reach services beyond Sim, send a Slack message, read a Gmail inbox, write a row to a CRM, call any API. Connect an account and share access with the people or workflows that need it. So the whole picture is small: a workspace is **data + processes**, integrations plug it into **everything else**, and the rest of this course is just learning each piece and watching them compose. ## Related documentation [#related-documentation] * [Introduction](/introduction) * [Getting started](/getting-started) --- # Choosing what to use (/agents/choosing) When you build an agent, several features overlap: a deterministic block and an agent tool can run the same integration, and a custom tool, an MCP tool, and a workflow-as-tool can all give an agent the same action. This page lays out the differences so you pick the right one. They vary along three lines: whether the action is **deterministic** (always runs) or **model-decided** (an agent chooses), whether it lives in one workflow or is **reusable** across your workspace, and whether it comes from Sim or an **external** provider. The running example is a workflow that scores inbound sales leads. It reads a new lead, enriches it, decides on a score, logs the result, and notifies the team. Each option below builds part of it: {/* VISUAL: decision tree. Always happens? → deterministic block. Else agent chooses? → agent tool. Reuse across workspace? → custom tool. External toolset? → MCP. Whole workflow? → workflow-as-tool. Reusable instructions? → skill. */} ## Deterministic block [#deterministic-block] A **block** is a single step that runs at a fixed point on the path, with no model deciding whether to. It always runs when the workflow reaches it. Use one when the action must happen every time: an API call, a data transform, a branch. In the lead scorer, a [Google Sheets](/integrations) block always appends the scored lead to a tracking sheet, and a [Function](/workflows/blocks/function) block always reshapes the enrichment response into the fields the next step expects. The block runs at that point in the graph; an execution error can still stop it. Most steps in a workflow are blocks. Reach for the kinds below only when you want a model to decide, or you want to reuse something. ## Agent tool [#agent-tool] An **agent tool** is an action you hand to an [Agent](/workflows/blocks/agent) block. The agent reads the task and decides whether and when to call it. The same catalog of [integrations](/integrations) that exist as standalone blocks can also be attached to an agent as tools. In the lead scorer, the Agent has a Search tool and a Send Email tool. For a lead with a thin profile it runs Search to gather context; for a strong lead it calls Send Email. A thin, obvious lead might trigger neither. The agent chooses per run. Each tool carries a `usageControl` setting. **Auto** lets the model decide (the default). **Force** makes the agent call the tool every run, for actions that should never be skipped, like always logging the decision. **None** removes the tool from that agent. A block and an agent tool can be the same underlying integration. The difference is who decides. A block runs because the path reached it. An agent tool runs because the agent chose it. ## Custom tool [#custom-tool] A **custom tool** is a tool you define once with an object schema and a JavaScript function body, then reuse across your workspace. It needs no external account. Use one when you have logic that several agents or workflows would otherwise duplicate. In the lead scorer, a `normalizeCompanyDomain` custom tool cleans a raw website into a canonical domain. The same tool serves the lead scorer, a deduplication workflow, and a reporting agent. Define it in the workspace, then pick it from any Agent block's tool list. ## MCP server [#mcp-server] **MCP** (Model Context Protocol) is a standard for connecting an external tool provider. Connect an [MCP server](/agents/mcp) and its tools appear in the agent's tool list as a set. Use it to bring in a complete toolset that Sim does not provide natively, rather than wiring each action by hand. In the lead scorer, your CRM vendor ships an MCP server. After you connect it once, the agent can read accounts and update records through the vendor's own tools. The difference from a custom tool is who maintains it: a custom tool is code you wrote, while an MCP server is a toolbox someone else maintains. ## Workflow-as-tool [#workflow-as-tool] A **workflow-as-tool** is a whole workflow handed to an agent as one callable tool. You pick the workflow in the [Agent](/workflows/blocks/agent) block's tool list; the agent decides when to call it and supplies the inputs, which arrive at the child's [Start](/workflows/triggers/start) trigger, and the child's result comes back as the tool's output. Use it when a multi-step procedure should be at the agent's disposal, not on the path. In the lead scorer, the agent has a `Deep Enrich` workflow as a tool — its own five-step procedure. For a thin lead, the agent calls it to fill out the profile before scoring; for a complete lead, it never runs. The agent weighs a whole procedure the same way it weighs a single action. A workflow can also run as a fixed step: the [Workflow](/workflows/blocks/workflow) block calls a child workflow because the path reached it. Same child workflow, same Start trigger — the difference, as with blocks and agent tools, is who decides. The Enrich step in the diagram is that deterministic case. ## Skill [#skill] A **skill** is reusable instructions, a written playbook an agent can follow. Each skill has a short name and description that are always visible to the agent, plus a longer body the agent loads only when it decides the skill applies. Use one to capture how something should be done, separate from the tools that do it. In the lead scorer, a `lead-scoring-rubric` skill spells out the bands and disqualifiers. The agent sees the skill's name and description on every run, and when a lead is ambiguous it loads the full rubric and applies it. The distinction is simple: a tool is an action the agent takes, and a skill is guidance the agent reads. Manage skills in your [workspace](/agents/skills). | Feature | Who decides it runs | Where it lives | How you author it | | -------------------- | ------------------- | ------------------ | --------------------------------------- | | **Block** | The path | The workflow | Drag in and configure | | **Agent tool** | The agent | On the Agent block | Pick from the integrations | | **Custom tool** | The agent | The workspace | Write the code once | | **MCP server** | The agent | An external server | Connect it | | **Workflow-as-tool** | The agent | Its own workflow | Build it, then pick it in the tool list | | **Skill** | The agent | The workspace | Write the instructions | --- # Custom Tools (/agents/custom-tools) Custom tools let you write your own JavaScript functions and make them available as callable tools in Agent blocks. This is useful when you need functionality that isn't covered by Sim's built-in integrations — for example, calling an internal API, performing a custom calculation, or transforming data in a specific way. ## How Custom Tools Work [#how-custom-tools-work] A custom tool has two parts: 1. **Schema** — A JSON definition describing the tool's name, description, and parameters (using the OpenAI function-calling format). This tells the AI agent what the tool does and what inputs it expects. 2. **Code** — A JavaScript function body that runs when the agent calls the tool. Parameters defined in the schema are available as variables in your code. When an Agent block has access to a custom tool, the AI model decides when to call it based on the schema description and the conversation context — just like built-in tools. ## Creating a Custom Tool [#creating-a-custom-tool] ### Open Custom Tools settings [#open-custom-tools-settings] Navigate to **Settings → Custom Tools** in your workspace and click **Add**. ### Define the schema [#define-the-schema] In the **Schema** tab, define your tool using JSON in the OpenAI function-calling format: ```json { "type": "function", "function": { "name": "get_weather", "description": "Get the current weather for a city", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "The city name" }, "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" } }, "required": ["city"] } } } ``` You can use the AI wand button to generate a schema from a natural language description of what the tool should do. ### Write the code [#write-the-code] Switch to the **Code** tab and write the JavaScript function body. Parameters from your schema are available directly as variables: ```javascript const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units === 'celsius' ? 'metric' : 'imperial'}&appid={{OPENWEATHER_API_KEY}}` ); const data = await response.json(); return { temperature: data.main.temp, description: data.weather[0].description, humidity: data.main.humidity }; ``` For a secret used as a complete JavaScript expression, prefer the unquoted form, such as `const apiKey = {{OPENWEATHER_API_KEY}};`. Quoted and embedded forms remain supported, including the placeholder embedded in the URL above, `"Bearer {{KEY}}"`, template literals, and JavaScript regex literals. The value is bound separately when the tool executes rather than pasted into its source, so its exact string contents are preserved. You can also use the AI wand to generate code from a description. Environment variables are referenced with `{{KEY}}` syntax. ### Save [#save] Click **Save** to create the tool. It's now available to use in any Agent block across your workspace. ## Using Custom Tools in Workflows [#using-custom-tools-in-workflows] Once created, custom tools appear alongside built-in tools when configuring an Agent block: 1. Open an Agent block 2. Click **Add Tools** 3. Find your custom tool in the tool list 4. The agent will call the tool when it determines it's relevant to the task ## Code Environment [#code-environment] ### Available Features [#available-features] * **Async/await** — Your code runs in an async context, so you can use `await` directly * **fetch()** — Make HTTP requests to external APIs * **Node.js built-ins** — Access to `crypto`, `Buffer`, and other standard modules * **Environment variables** — Use `{{KEY}}` syntax to bind secrets at execution time without placing plaintext in source code ### Limitations [#limitations] * **No npm packages** — External libraries like `axios` or `lodash` are not available. Use built-in APIs instead * **Parameters by name** — Schema parameters are available directly as variables (e.g., `city`), not via a `params` object ### Returning Results [#returning-results] Return a value from your code to send it back to the agent: ```javascript const result = await fetch(`https://api.example.com/data?q=${query}`); const data = await result.json(); return data; ``` The returned value becomes the tool output that the agent sees and can use in its response. ## Managing Custom Tools [#managing-custom-tools] From **Settings → Custom Tools** you can: * **Search** tools by name, function name, or description * **Edit** any tool's schema or code * **Delete** tools that are no longer needed Deleting a custom tool removes it from all Agent blocks that reference it. Make sure no active workflows depend on the tool before deleting. ## Permissions [#permissions] | Action | Required Permission | | -------------------- | --------------------------------- | | View custom tools | **Read**, **Write**, or **Admin** | | Create or edit tools | **Write** or **Admin** | | Delete tools | **Admin** | --- # Agents (/agents) An **agent** is a [workflow](/workflows) that reasons and acts on its own. It reads an input, decides what to do, and carries it out, calling tools and using your data along the way. You build a custom agent in Sim by composing a workflow whose thinking runs through one or more [Agent blocks](/workflows/blocks/agent). An **Agent block** is the reasoning step inside that workflow: a model reads the values available to it, decides, and returns a result that later blocks read by reference. A simple agent is a single Agent block; a larger one wires several together with other blocks. The Agent block is where the model thinks; the rest of the workflow is what it acts on and through. The example throughout is an agent that scores inbound sales leads. ## The Agent block [#the-agent-block] You set up the reasoning step by giving the Agent block a **model** and a **prompt**. The model is the LLM that powers it; you pick one from the available providers, and the default is `claude-sonnet-4-6`. The prompt is a system message that defines who the agent is and how it should behave, plus a user message that carries the input, usually a reference like ``. When it runs, the Agent block reasons, calls any tools it needs, and stores its result under its own name. By default that result is free text in `content`, read by a later block as ``, alongside run details like the model used, token counts, tool calls, and cost. Every setting and output field is in the [Agent block reference](/workflows/blocks/agent). ## What you give an agent [#what-you-give-an-agent] On its own, an Agent block can only reason and write text. You extend it so it can act, follow your rules, use your data, remember, and return results other blocks can rely on. Each feature maps to something you'd want an agent to do. ### Take an action: tools [#take-an-action-tools] To let an agent do something in the world, give it **tools**. A tool is an action the agent can call, like sending an email, searching the web, updating a CRM record, or running another workflow. You attach tools to the Agent block, and the agent decides which to call for the task in front of it. In the lead scorer, the agent has a search tool to gather context on a thin profile and an email tool to reach out to a strong lead. Tools come from a few places: * **[Integrations](/integrations)** are the catalog of external services: Gmail, Slack, Airtable, Linear, and hundreds more. * **[Custom tools](/agents/custom-tools)** are tools you define once with a schema and a snippet of code, then reuse. * **[MCP tools](/agents/mcp)** come from an external provider you connect through the Model Context Protocol. * **[Workflow-as-tool](/workflows)** makes another workflow callable, so the agent runs a whole procedure as one step. The same integration can run two ways. As a [block](/workflows#blocks) it runs because the path reached it. As an agent tool it runs because the agent chose it. Per-tool usage controls (force a tool, or disable one) are in the [Agent block reference](/workflows/blocks/agent). ### Follow a procedure: skills [#follow-a-procedure-skills] To give an agent instructions it can follow, write a [skill](/agents/skills). A skill is a reusable playbook with a short name and description the agent always sees, plus a longer body it loads only when the skill applies. In the lead scorer, a `lead-scoring-rubric` skill spells out the bands and disqualifiers, and the agent reads the full rubric only when a lead is ambiguous. A tool is an action the agent takes; a skill is guidance it reads. ### Use your documents: knowledge [#use-your-documents-knowledge] To let an agent answer from your own content, connect a [knowledge base](/knowledgebase). The agent searches it and grounds its answers in what it finds, instead of relying only on the model's general training. Give the lead scorer a knowledge base of past deals and it can compare a new lead against ones you've closed before. ### Remember across runs: memory [#remember-across-runs-memory] To let an agent reuse information from one run to the next, give it [memory](/workflows/blocks/agent#memory), which stores and recalls values keyed to a conversation. Without it, each run starts fresh; with it, an agent in a chat carries what was said earlier into later messages. ### Return a usable result: structured output [#return-a-usable-result-structured-output] To make an agent's result something later blocks can act on, give it a **structured output**: a typed object you define instead of free text. In the lead scorer, the agent returns `{ score, tier, reason }`, and a later [Condition](/workflows/blocks/condition) block reads `` to branch. See [how blocks pass data](/workflows/data-flow) for reading fields. ## Choosing what to use [#choosing-what-to-use] Start with one Agent block and a prompt, then add only what the task needs: a tool when the agent should act, a skill when it needs written guidance, a knowledge base when it should answer from your documents, memory when it should remember, and a structured output when a later block has to read its result. --- # Using MCP tools (/agents/mcp) The Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) is an open standard for connecting AI to external tools and data. Add an MCP server to your workspace and its tools become available to your agents — a way to integrate services Sim doesn't have a built-in integration for. ## Adding an MCP Server as a Tool [#adding-an-mcp-server-as-a-tool] MCP servers provide collections of tools that your agents can use.
To add one: 1. Navigate to **Settings → MCP Tools**
MCP Tools settings page
2. Click **Add** to open the configuration modal 3. Enter a **Server Name** and **Server URL** 4. Add any required **Headers** (e.g. API keys) 5. Click **Add MCP** to save
Add New MCP Server modal
You can also configure MCP servers directly from the toolbar in an Agent block for quick setup. ### Server Configuration Options [#server-configuration-options] | Field | Description | | ------------- | ---------------------------------------------------- | | **Name** | Display name for the server | | **URL** | The MCP server endpoint | | **Transport** | Currently supports `streamable-http` | | **Headers** | Key-value pairs for authentication or custom headers | | **Timeout** | Connection timeout in milliseconds (default: 30,000) | ### Environment Variables in Configuration [#environment-variables-in-configuration] Server URLs and headers support environment variable substitution using `{{VAR_NAME}}` syntax. This keeps sensitive values like API keys out of the server configuration. ``` URL: https://api.example.com/mcp Authorization: Bearer {{MCP_API_TOKEN}} ``` When you type `{{` in the URL or header fields, a dropdown appears showing available workspace environment variables. When a saved secret is successfully substituted this way, exact occurrences of its value are masked in stored MCP tool-call traces. The real URL or header value still reaches the MCP server unchanged. See [Execution log protection](/platform/credentials#execution-log-protection) for the exact scope and limitations. ### Testing and Validation [#testing-and-validation] Click **Test Connection** before saving to verify the server is reachable and discover available tools. The test response shows the number of tools found and the protocol version. After saving, each server displays its available tools with parameter names, types, and required flags. If a server's tools change (e.g., after a server update), click **Refresh** to fetch the latest schemas. This automatically updates any agent blocks using those tools. Tool validation badges appear on servers with issues — for example, if a tool was removed from the server but is still referenced in a workflow. Click the badge to see which workflows are affected. ### Domain Allowlisting [#domain-allowlisting] Self-hosted deployments can restrict which MCP server domains are allowed by setting the `ALLOWED_MCP_DOMAINS` environment variable (comma-separated list). When set, only servers on approved domains can be added. When unset, all domains are allowed. This governs which domains may be used. It is separate from where those domains are allowed to resolve: an MCP server on a private address is reached by naming it in `EGRESS_ALLOWED_HOSTS` or `EGRESS_ALLOWED_IP_RANGES`, described in [Security](/platform/self-hosting/security#the-ssrf-boundary). Both checks apply. The allowlist covers the server URL itself. If the server requires OAuth, any endpoint its metadata names on a *different* origin than the server you configured is treated as content rather than as configuration, so that one has to be publicly routable. Endpoints on the server's own origin keep the server's reachability. ## Using MCP Tools in Agents [#using-mcp-tools-in-agents] Once MCP servers are configured, their tools become available within your agent blocks:
Using MCP Tool in Agent Block
1. Open an **Agent** block 2. In the **Tools** section, click **Add tool…** 3. Under **MCP Servers**, click a server to see its tools
MCP tools list for a selected server
4. Select individual tools, or choose **Use all N tools** to add every tool from that server 5. The agent can now access these tools during execution If you haven't configured a server yet, click **Add MCP Server** at the top of the dropdown to open the setup modal without leaving the block. ## Standalone MCP Tool Block [#standalone-mcp-tool-block] For more granular control, you can use the dedicated MCP Tool block to execute specific MCP tools:
Standalone MCP Tool Block
The MCP Tool block runs one configured tool with parameters you set explicitly, and its output is readable by later blocks like any other. ## When to Use MCP Tool vs Agent [#when-to-use-mcp-tool-vs-agent] | Feature | **Agent with MCP tools** | **MCP Tool block** | | -------------- | -------------------------------- | -------------------------------------- | | **Execution** | AI decides which tools to call | Deterministic — runs the tool you pick | | **Parameters** | AI chooses at runtime | You set them explicitly | | **Best for** | Dynamic, conversational flows | Structured, repeatable steps | | **Reasoning** | Handles complex multi-step logic | One tool, one call | ## Permission Requirements [#permission-requirements] MCP functionality requires specific workspace permissions: | Action | Required Permission | | ---------------------------- | --------------------------------- | | Create or update MCP servers | **Write** or **Admin** | | Delete MCP servers | **Admin** | | Use MCP tools in agents | **Write** or **Admin** | | View available MCP tools | **Read**, **Write**, or **Admin** | | Execute MCP Tool blocks | **Read**, **Write**, or **Admin** | MCP servers run with the permissions of the user who configured them — only connect servers you trust, and review a server's tools before handing them to agents. --- # Agent skills (/agents/skills) Agent Skills are reusable packages of instructions that give your AI agents specialized capabilities. Based on the open [Agent Skills](https://agentskills.io) format, skills let you capture domain expertise, workflows, and best practices that agents can load on demand. ## How Skills Work [#how-skills-work] Skills use **progressive disclosure** to keep agent context lean: 1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (\~50-100 tokens each) 2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context 3. **Execution** — The agent follows the loaded instructions to complete the task ## Creating Skills [#creating-skills] Skills live on the **Integrations** page: click **Integrations** in the workspace sidebar, then switch to the **Skills** tab. It lists every skill in the workspace, searchable by name. Click a skill to open its detail page, where you edit, share, and delete it. The Skills tab on the Integrations page Click **+ Add to Sim** to open the skill create page, which takes three fields: | Field | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. | | **Description** | A short explanation of what the skill does and when to use it. This is what the agent reads to decide whether to activate the skill. Max 1024 characters. | | **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. | The description is critical — it's the only thing the agent sees before deciding to load a skill. Be specific about when and why the skill should be used. ### Importing skills [#importing-skills] Bring in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format two ways: * **Import** — the **Import** action on the create page takes a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`. * **Paste content** — paste the `SKILL.md` straight into **Content**. The frontmatter carries the `name` and `description`; the markdown body is the content. Integration pages suggest **curated skills** for their service — open one (HubSpot, for example) and add a suggested skill with one click. ### Writing Good Skill Content [#writing-good-skill-content] Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification): ```markdown # SQL Expert ## When to use this skill Use when the user asks you to write, optimize, or debug SQL queries. ## Instructions 1. Always ask which database engine (PostgreSQL, MySQL, SQLite) 2. Use CTEs over subqueries for readability 3. Add index recommendations when relevant 4. Explain query plans for optimization requests ## Common Patterns ... ``` **Recommended structure:** * **When to use** — Specific triggers and scenarios * **Instructions** — Step-by-step guidance with numbered lists * **Examples** — Input/output samples showing expected behavior * **Common Patterns** — Reusable approaches for frequent tasks * **Edge Cases** — Gotchas and special considerations Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills. ## Skill Editors [#skill-editors] Everyone in the workspace sees and uses every skill — including members who join later. Nobody needs to be added to a skill to use it. Each skill has an explicit **editors** list. Editors can edit the skill, delete it, and manage the editors list. Workspace admins can always do this too — they are editors of every skill automatically and cannot be removed from the list. Whoever creates a skill becomes an editor. Open a skill from the Skills tab to manage it. The detail page has the editable fields, a **Share** action for adding editors from your workspace members, and the **Skill Editors** list at the bottom. The editors list controls who can edit a skill — it never affects who can see, use, or run it. A workflow that references a skill always executes it, no matter who runs the workflow. Treat skill content as shared team instructions, not as a secret. ## Using Skills in Chat [#using-skills-in-chat] Skills work in Chat too. Type `/` in the message box to open the skills menu, then pick a skill — or keep typing to filter by name. The skill appears in your message as a tag, e.g. `/format-markdown`. Tagging a skill loads its full instructions into the conversation, so Sim follows them for that request — no waiting for Sim to decide the skill is relevant on its own. ## Adding Skills to an Agent [#adding-skills-to-an-agent] Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to. Add Skill Selected skills appear as cards that you can click to edit or remove. ### What Happens at Runtime [#what-happens-at-runtime] When the workflow runs: 1. The agent's system prompt includes an `` section listing each skill's name and description 2. A `load_skill` tool is automatically added to the agent's available tools 3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name 4. The full skill content is returned as a tool response, giving the agent detailed instructions This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed. ## When to use a skill [#when-to-use-a-skill] Use a skill for reusable procedures or knowledge shared across agents, such as a company style guide or a debugging checklist. Keep task-specific instructions in the Agent block. ## Best Practices [#best-practices] **Writing Effective Descriptions** * **Be specific and keyword-rich** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis" * **Include activation triggers** — Mention specific words or phrases that should prompt the skill (e.g., "Use when the user mentions PDFs, forms, or document extraction") * **Keep it under 200 words** — Agents scan descriptions quickly; make every word count **Skill Scope and Organization** * **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill * **Limit to 5-10 skills per agent** — More skills = more decision overhead; start small and add as needed * **Split large skills** — If a skill exceeds 500 lines, break it into focused sub-skills **Content Structure** * **Use markdown formatting** — Headers, lists, and code blocks help agents parse and follow instructions * **Provide examples** — Show input/output pairs so agents understand expected behavior * **Be explicit about edge cases** — Don't assume agents will infer special handling **Testing and Iteration** * **Test activation** — Run your workflow and verify the agent loads the skill when expected * **Check for false positives** — Make sure skills aren't activating when they shouldn't * **Refine descriptions** — If a skill isn't loading when needed, add more keywords to the description ## Learn More [#learn-more] * [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills * [Example skills](https://github.com/anthropics/skills) — Browse community skill examples * [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills --- # Authentication (/api-reference/authentication) The Sim API accepts API keys and, when enabled by your deployment, OAuth access tokens. API keys support automation and the SDKs. OAuth lets the CLI and registered applications act on your behalf with permissions you approve. Sim supports two types of API keys — **personal keys** and **workspace keys** — each with different billing and access behaviors. ## Key Types [#key-types] | Feature | **Personal Keys** | **Workspace Keys** | | --------------- | ------------------------------------------ | --------------------------- | | **Billing** | Workspace payer for workspace-hosted usage | Workspace payer | | **Scope** | Across workspaces you have access to | Shared across the workspace | | **Managed by** | Each user individually | Workspace admins | | **Permissions** | Must be enabled at workspace level | Require admin permissions | Personal keys identify the user making a request; they do not select who pays. Hosted usage is billed to the workspace's organization or personal billing account and, for organizations, is attributed to the actor's member cap. Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used. ## Generating API Keys [#generating-api-keys] To generate a personal key, open **Account settings** → **Sim API keys**. Workspace administrators can create shared keys from **Workspace settings** → **Sim API keys**. API keys are only shown once when generated. Store your key securely — you will not be able to view it again. ## Using API Keys [#using-api-keys] Pass your API key in the `X-API-Key` header with every request: ```bash curl -X POST https://www.sim.ai/api/v2/workflows/{workflowId}/execute \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"input": {}}' ``` ```typescript const response = await fetch( 'https://www.sim.ai/api/v2/workflows/{workflowId}/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SIM_API_KEY!, }, body: JSON.stringify({ input: {} }), } ) ``` ```python import requests response = requests.post( "https://www.sim.ai/api/v2/workflows/{workflowId}/execute", headers={ "Content-Type": "application/json", "X-API-Key": os.environ["SIM_API_KEY"], }, json={"input": {}}, ) ``` ## Where Keys Are Used [#where-keys-are-used] API keys authenticate access to: * **Workflow execution** — run deployed workflows via the API * **Logs API** — query workflow execution logs and metrics * **MCP servers** — authenticate connections to deployed MCP servers * **SDKs** — the [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) SDKs use API keys for all operations ## OAuth access tokens [#oauth-access-tokens] Use `sim login` to authorize the CLI in your browser, or `sim login --read-only` to request read access. The CLI stores the login locally and refreshes access tokens automatically. See [CLI authentication](/cli/authentication) for profiles, sign-in, and sign-out. Registered OAuth applications send access tokens in the `Authorization` header: ```bash curl https://www.sim.ai/api/v2/workspaces \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` | Scope | Access | | ---------------- | ---------------------------------------------------------------------------------------------------- | | `api:read` | Read operations, including searches sent as POST requests | | `api:write` | Includes `api:read`, plus mutations and execution, including operations that can start external work | | `offline_access` | Refresh tokens for continued access after the access token expires | Scopes limit what an application may do; your current workspace membership and role still apply. Each endpoint documents its required scope. Some GET endpoints that perform external discovery require `api:write`, so HTTP method alone does not determine the permission. Manage grants in **Settings** → **General** → **Authorized apps**. Revoking an application signs out all of its logins. `sim logout` revokes the current CLI login and removes it from your machine. The Python and TypeScript SDKs currently use API keys; they do not manage OAuth sign-in or refresh tokens. ## Security [#security] * Keys use the `sk-sim-` prefix and are encrypted at rest * Keys can be revoked at any time from the dashboard * Use environment variables to store keys — never hardcode them in source code * For browser-based applications, use a backend proxy to avoid exposing keys to the client Never expose your API key in client-side code. Use a server-side proxy to make authenticated requests on behalf of your frontend. --- # Getting Started (/api-reference/getting-started) ## Base URL [#base-url] All API requests are made to: ``` https://www.sim.ai ``` ## OpenAPI specification [#openapi-specification] Download the [complete OpenAPI 3.1 specification](/openapi.json) as JSON for client generation, request validation, and API tooling. ## Quick Start [#quick-start] ### Get your API key [#get-your-api-key] Open **Account settings** → **Sim API keys** to create a personal key, or **Workspace settings** → **Sim API keys** for a workspace key. These examples and the SDKs use API keys; the CLI also supports browser sign-in with `sim login`. See [Authentication](/api-reference/authentication) for key types and OAuth permissions. ### Find your workflow ID [#find-your-workflow-id] Open a workflow in the Sim editor. The workflow ID is in the URL: ``` https://www.sim.ai/workspace/{workspaceId}/w/{workflowId} ``` You can also use the [List Workflows](/api-reference/workflows/listWorkflows) endpoint to get all workflow IDs in a workspace. ### Deploy your workflow [#deploy-your-workflow] A workflow must be deployed before it can be executed via the API. Click the **Deploy** button in the editor toolbar, or use the dashboard to manage deployments. ### Make your first request [#make-your-first-request] ```bash curl -X POST https://www.sim.ai/api/v2/workflows/{workflowId}/execute \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"input": {}}' ``` ```typescript const response = await fetch( `https://www.sim.ai/api/v2/workflows/${workflowId}/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.SIM_API_KEY!, }, body: JSON.stringify({ input: {} }), } ) const data = await response.json() console.log(data.data.output) ``` ```python import requests import os response = requests.post( f"https://www.sim.ai/api/v2/workflows/{workflow_id}/execute", headers={ "Content-Type": "application/json", "X-API-Key": os.environ["SIM_API_KEY"], }, json={"input": {}}, ) data = response.json() print(data["data"]["output"]) ``` ## Sync vs Async Execution [#sync-vs-async-execution] By default, workflow executions are **synchronous** — the API blocks until the workflow completes and returns the result directly. For long-running workflows, use **asynchronous execution** by passing `async: true`: ```bash curl -X POST https://www.sim.ai/api/v2/workflows/{workflowId}/execute \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"input": {}, "async": true}' ``` Keyed callers can optionally provide `X-Run-Id: my-run-123` to choose the run ID. Run IDs cannot be reused; a duplicate returns `409`. This returns immediately with a `runId` and `statusUrl`: ```json { "data": { "runId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", "statusUrl": "https://www.sim.ai/api/v2/workflows/{workflowId}/runs/c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74" } } ``` Poll the run status endpoint until the status is terminal: ```bash curl https://www.sim.ai/api/v2/workflows/{workflowId}/runs/{runId}?includeOutput=true \ -H "X-API-Key: YOUR_API_KEY" ``` Execution status transitions follow: `queued` → `running` → `completed`, `failed`, `cancelled`, or `paused`. The `data.output` field is populated for completed executions when `includeOutput=true`. ## Response Format [#response-format] Successful v2 responses wrap the run resource in `data`: ```json { "data": { "runId": "c7a92e15-3f4b-4d8c-a1e6-9b0d5f2c8e74", "workflowId": "{workflowId}", "status": "completed", "output": { "result": "Hello, world!" }, "error": null, "durationMs": 842 } } ``` ## Error Handling [#error-handling] The API uses standard HTTP status codes. v2 errors include a stable code and human-readable message: ```json { "error": { "code": "NOT_FOUND", "message": "Workflow not found" } } ``` | Status | Meaning | What to do | | ------ | -------------------------- | --------------------------------------------------- | | `400` | Invalid request parameters | Check the `details` array for specific field errors | | `401` | Missing or invalid API key | Verify your `X-API-Key` header | | `403` | Access denied | Check you have permission for this resource | | `404` | Resource not found | Verify the ID exists and belongs to your workspace | | `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header | ### Unrecognized fields are rejected [#unrecognized-fields-are-rejected] Every v2 endpoint validates the request against its published schema — path parameters, query string, and body — and answers `400` for any field it does not declare. A misspelled parameter is an error rather than a silent no-op, so `?limt=20` fails instead of quietly returning an unbounded list. This holds for endpoints that declare no query parameters at all. Do not append tracking tags, cache busters, or other extra parameters to a v2 URL; send only what the endpoint documents. ```json { "error": { "code": "BAD_REQUEST", "message": "Invalid request", "details": [ { "code": "unrecognized_keys", "keys": ["limt"], "path": [], "message": "Unrecognized key: \"limt\"" } ] } } ``` Use [Get Billing Status](/api-reference/billing/getBillingStatus) to inspect current credit and storage usage. ## Rate Limits [#rate-limits] Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions. When rate limited, the API returns a `429` response with a `Retry-After` header indicating how many seconds to wait before retrying. ## Pagination [#pagination] List endpoints (workflows, logs, audit logs) use **cursor-based pagination**: ```bash # First page curl "https://www.sim.ai/api/v2/logs?limit=20" \ -H "X-API-Key: YOUR_API_KEY" # Next page — use the nextCursor from the previous response curl "https://www.sim.ai/api/v2/logs?limit=20&cursor=abc123" \ -H "X-API-Key: YOUR_API_KEY" ``` The response includes a `nextCursor` field. When `nextCursor` is absent or `null`, you have reached the last page. --- # Python (/api-reference/python) Use the Python SDK to execute workflows from Python applications. The Python SDK supports Python 3.8+ with async execution support, retry helpers with exponential backoff, and usage tracking. ## Installation [#installation] Install the SDK using pip: ```bash pip install simstudio-sdk ``` ## Quick Start [#quick-start] Here's a simple example to get you started: ```python from simstudio import SimStudioClient # Initialize the client client = SimStudioClient( api_key="your-api-key-here", base_url="https://sim.ai" # optional, defaults to https://sim.ai ) # Execute a workflow try: result = client.execute_workflow("workflow-id") print("Workflow executed successfully:", result) except Exception as error: print("Workflow execution failed:", error) ``` ## API Reference [#api-reference] ### SimStudioClient [#simstudioclient] #### Constructor [#constructor] ```python SimStudioClient(api_key: str, base_url: str = "https://sim.ai") ``` **Parameters:** * `api_key` (str): Your Sim API key * `base_url` (str, optional): Base URL for the Sim API #### Methods [#methods] ##### execute\_workflow() [#execute_workflow] Execute a workflow with optional input data. ```python result = client.execute_workflow( "workflow-id", input={"message": "Hello, world!"}, timeout=30.0 # 30 seconds ) ``` **Parameters:** * `workflow_id` (str): The ID of the workflow to execute * `input` (dict, optional): Input data to pass to the workflow * `timeout` (float, optional): Timeout in seconds (default: 30.0) * `stream` (bool, optional): Enable streaming responses (default: False) * `selected_outputs` (list\[str], optional): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`) * `async_execution` (bool, optional): Execute asynchronously (default: False) * `execution_timeout_seconds` (int, optional): Optional server-side async execution cap from 1 to 604800 seconds. Requires `async_execution=True` and cannot extend the account policy. **Returns:** `WorkflowExecutionResult | AsyncExecutionResult` When `async_execution=True`, returns immediately with a `run_id` and `status_url` for polling. Otherwise, waits for completion. ##### get\_workflow\_status() [#get_workflow_status] Get the status of a workflow (deployment status, etc.). ```python status = client.get_workflow_status("workflow-id") print("Is deployed:", status.is_deployed) ``` **Parameters:** * `workflow_id` (str): The ID of the workflow **Returns:** `WorkflowStatus` ##### validate\_workflow() [#validate_workflow] Validate that a workflow is ready for execution. ```python is_ready = client.validate_workflow("workflow-id") if is_ready: # Workflow is deployed and ready pass ``` **Parameters:** * `workflow_id` (str): The ID of the workflow **Returns:** `bool` ##### get\_workflow\_run() [#get_workflow_run] Get the status and optional outputs of a workflow execution. ```python status = client.get_workflow_run("workflow-id", "run-id", include_output=True) print("Status:", status["status"]) # 'queued', 'running', 'completed', 'failed' if status["status"] == "completed": print("Output:", status["output"]) ``` **Parameters:** * `workflow_id` (str): The workflow ID * `run_id` (str): The run ID returned from async execution * `include_output` (bool, optional): Include the final output for completed executions * `selected_outputs` (list\[str], optional): Block output selectors to include **Returns:** `Dict[str, Any]` **Response fields:** * `runId` (str): The run ID * `workflowId` (str): The workflow ID * `status` (str): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` * `startedAt` / `endedAt` (str): Execution timestamps * `durationMs` (int, optional): Duration in milliseconds * `output` (any, optional): The workflow output when requested for a completed execution * `blockOutputs` (dict, optional): Requested block outputs * `error` (dict, optional): Structured failure details with `code`, `message`, and optional `details` ##### get\_job\_status() [#get_job_status] Get the status of a job created through the legacy async execution endpoint. New integrations should use `get_workflow_run()` with the run ID instead. ```python status = client.get_job_status("legacy-job-id") ``` ##### execute\_with\_retry() [#execute_with_retry] Execute a workflow with automatic retry on rate limit errors using exponential backoff. ```python result = client.execute_with_retry( "workflow-id", input={"message": "Hello"}, timeout=30.0, max_retries=3, # Maximum number of retries initial_delay=1.0, # Initial delay in seconds max_delay=30.0, # Maximum delay in seconds backoff_multiplier=2.0 # Exponential backoff multiplier ) ``` **Parameters:** * `workflow_id` (str): The ID of the workflow to execute * `input` (dict, optional): Input data to pass to the workflow * `timeout` (float, optional): Timeout in seconds * `stream` (bool, optional): Enable streaming responses * `selected_outputs` (list, optional): Block outputs to stream * `async_execution` (bool, optional): Execute asynchronously * `max_retries` (int, optional): Maximum number of retries (default: 3) * `initial_delay` (float, optional): Initial delay in seconds (default: 1.0) * `max_delay` (float, optional): Maximum delay in seconds (default: 30.0) * `backoff_multiplier` (float, optional): Backoff multiplier (default: 2.0) **Returns:** `WorkflowExecutionResult | AsyncExecutionResult` The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a `retry-after` header, it will be used instead. ##### get\_rate\_limit\_info() [#get_rate_limit_info] Get the current rate limit information from the last API response. ```python rate_limit_info = client.get_rate_limit_info() if rate_limit_info: print("Limit:", rate_limit_info.limit) print("Remaining:", rate_limit_info.remaining) print("Reset:", datetime.fromtimestamp(rate_limit_info.reset)) ``` **Returns:** `RateLimitInfo | None` ##### get\_usage\_limits() [#get_usage_limits] Get current usage limits and quota information for your account. ```python limits = client.get_usage_limits() print("Sync requests remaining:", limits.rate_limit["sync"]["remaining"]) print("Async requests remaining:", limits.rate_limit["async"]["remaining"]) print("Current period cost:", limits.usage["currentPeriodCost"]) print("Plan:", limits.usage["plan"]) ``` **Returns:** `UsageLimits` **Response structure:** ```python { "success": bool, "rateLimit": { "sync": { "isLimited": bool, "limit": int, "remaining": int, "resetAt": str }, "async": { "isLimited": bool, "limit": int, "remaining": int, "resetAt": str }, "authType": str # 'api' or 'manual' }, "usage": { "currentPeriodCost": float, "limit": float, "plan": str # e.g., 'free', 'pro' } } ``` ##### set\_api\_key() [#set_api_key] Update the API key. ```python client.set_api_key("new-api-key") ``` ##### set\_base\_url() [#set_base_url] Update the base URL. ```python client.set_base_url("https://my-custom-domain.com") ``` ##### close() [#close] Close the underlying HTTP session. ```python client.close() ``` ## Data Classes [#data-classes] ### WorkflowExecutionResult [#workflowexecutionresult] ```python @dataclass class WorkflowExecutionResult: success: bool output: Optional[Any] = None error: Optional[str] = None logs: Optional[List[Any]] = None metadata: Optional[Dict[str, Any]] = None trace_spans: Optional[List[Any]] = None total_duration: Optional[float] = None status: Optional[str] = None ``` `success` is `True` only for the `completed` and `paused` statuses. `status` carries the server's terminal status verbatim, so a cancelled run (`success=False`, `error=None`) is distinguishable from a failed one. ### AsyncExecutionResult [#asyncexecutionresult] ```python @dataclass class AsyncExecutionResult: success: bool run_id: str status_url: str message: str = "" async_execution: bool = True ``` ### WorkflowStatus [#workflowstatus] ```python @dataclass class WorkflowStatus: is_deployed: bool deployed_at: Optional[str] = None needs_redeployment: bool = False ``` ### RateLimitInfo [#ratelimitinfo] ```python @dataclass class RateLimitInfo: limit: int remaining: int reset: int retry_after: Optional[int] = None ``` ### UsageLimits [#usagelimits] ```python @dataclass class UsageLimits: success: bool rate_limit: Dict[str, Any] usage: Dict[str, Any] ``` ### SimStudioError [#simstudioerror] ```python class SimStudioError(Exception): def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None): super().__init__(message) self.code = code self.status = status ``` **Common error codes:** * `UNAUTHORIZED`: Invalid API key * `TIMEOUT`: Request timed out * `RATE_LIMIT_EXCEEDED`: Rate limit exceeded * `USAGE_LIMIT_EXCEEDED`: Usage limit exceeded * `EXECUTION_ERROR`: Workflow execution failed ## Examples [#examples] ### Basic Workflow Execution [#basic-workflow-execution] Set up the SimStudioClient with your API key. Check if the workflow is deployed and ready for execution. Run the workflow with your input data. Process the execution result and handle any errors. ```python import os from simstudio import SimStudioClient client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def run_workflow(): try: # Check if workflow is ready is_ready = client.validate_workflow("my-workflow-id") if not is_ready: raise Exception("Workflow is not deployed or ready") # Execute the workflow result = client.execute_workflow( "my-workflow-id", input={ "message": "Process this data", "user_id": "12345" } ) if result.success: print("Output:", result.output) print("Duration:", result.metadata.get("duration") if result.metadata else None) else: print("Workflow failed:", result.error) except Exception as error: print("Error:", error) run_workflow() ``` ### Error Handling [#error-handling] Handle different types of errors that may occur during workflow execution: ```python from simstudio import SimStudioClient, SimStudioError import os client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def execute_with_error_handling(): try: result = client.execute_workflow("workflow-id") return result except SimStudioError as error: if error.code == "UNAUTHORIZED": print("Invalid API key") elif error.code == "TIMEOUT": print("Workflow execution timed out") elif error.code == "USAGE_LIMIT_EXCEEDED": print("Usage limit exceeded") elif error.code == "INVALID_JSON": print("Invalid JSON in request body") else: print(f"Workflow error: {error}") raise except Exception as error: print(f"Unexpected error: {error}") raise ``` ### Context Manager Usage [#context-manager-usage] Use the client as a context manager to automatically handle resource cleanup: ```python from simstudio import SimStudioClient import os # Using context manager to automatically close the session with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client: result = client.execute_workflow("workflow-id") print("Result:", result) # Session is automatically closed here ``` ### Batch Workflow Execution [#batch-workflow-execution] Execute multiple workflows efficiently: ```python from simstudio import SimStudioClient import os client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def execute_workflows_batch(workflow_data_pairs): """Execute multiple workflows with different input data.""" results = [] for workflow_id, input_data in workflow_data_pairs: try: # Validate workflow before execution if not client.validate_workflow(workflow_id): print(f"Skipping {workflow_id}: not deployed") continue result = client.execute_workflow(workflow_id, input_data) results.append({ "workflow_id": workflow_id, "success": result.success, "output": result.output, "error": result.error }) except Exception as error: results.append({ "workflow_id": workflow_id, "success": False, "error": str(error) }) return results # Example usage workflows = [ ("workflow-1", {"type": "analysis", "data": "sample1"}), ("workflow-2", {"type": "processing", "data": "sample2"}), ] results = execute_workflows_batch(workflows) for result in results: print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}") ``` ### Async Workflow Execution [#async-workflow-execution] Execute workflows asynchronously for long-running tasks: ```python import os import time from simstudio import SimStudioClient client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def execute_async(): try: # Start async execution result = client.execute_workflow( "workflow-id", input={"data": "large dataset"}, async_execution=True # Execute asynchronously ) # Check if result is an async execution if hasattr(result, 'async_execution') and result.async_execution: print(f"Run ID: {result.run_id}") print(f"Status endpoint: {result.status_url}") # Poll for completion status = client.get_workflow_run( "workflow-id", result.run_id, include_output=True ) while status["status"] in ["queued", "pending", "running"]: print(f"Current status: {status['status']}") time.sleep(2) # Wait 2 seconds status = client.get_workflow_run( "workflow-id", result.run_id, include_output=True ) if status["status"] == "completed": print("Workflow completed!") print(f"Output: {status['output']}") print(f"Duration: {status['durationMs']}") elif status["status"] == "paused": print("Workflow is paused and waiting for input or resumption.") elif status["status"] == "cancelled": print("Workflow was cancelled.") else: print(f"Workflow failed: {status['error']}") except Exception as error: print(f"Error: {error}") execute_async() ``` ### Rate Limiting and Retry [#rate-limiting-and-retry] Handle rate limits automatically with exponential backoff: ```python import os from simstudio import SimStudioClient, SimStudioError client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def execute_with_retry_handling(): try: # Automatically retries on rate limit result = client.execute_with_retry( "workflow-id", input={"message": "Process this"}, max_retries=5, initial_delay=1.0, max_delay=60.0, backoff_multiplier=2.0 ) print(f"Success: {result}") except SimStudioError as error: if error.code == "RATE_LIMIT_EXCEEDED": print("Rate limit exceeded after all retries") # Check rate limit info rate_limit_info = client.get_rate_limit_info() if rate_limit_info: from datetime import datetime reset_time = datetime.fromtimestamp(rate_limit_info.reset) print(f"Rate limit resets at: {reset_time}") execute_with_retry_handling() ``` ### Usage Monitoring [#usage-monitoring] Monitor your account usage and limits: ```python import os from simstudio import SimStudioClient client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def check_usage(): try: limits = client.get_usage_limits() print("=== Rate Limits ===") print("Sync requests:") print(f" Limit: {limits.rate_limit['sync']['limit']}") print(f" Remaining: {limits.rate_limit['sync']['remaining']}") print(f" Resets at: {limits.rate_limit['sync']['resetAt']}") print(f" Is limited: {limits.rate_limit['sync']['isLimited']}") print("\nAsync requests:") print(f" Limit: {limits.rate_limit['async']['limit']}") print(f" Remaining: {limits.rate_limit['async']['remaining']}") print(f" Resets at: {limits.rate_limit['async']['resetAt']}") print(f" Is limited: {limits.rate_limit['async']['isLimited']}") print("\n=== Usage ===") print(f"Current period cost: ${limits.usage['currentPeriodCost']:.2f}") print(f"Limit: ${limits.usage['limit']:.2f}") print(f"Plan: {limits.usage['plan']}") percent_used = (limits.usage['currentPeriodCost'] / limits.usage['limit']) * 100 print(f"Usage: {percent_used:.1f}%") if percent_used > 80: print("⚠️ Warning: You are approaching your usage limit!") except Exception as error: print(f"Error checking usage: {error}") check_usage() ``` ### Streaming Workflow Execution [#streaming-workflow-execution] Execute workflows with real-time streaming responses: ```python from simstudio import SimStudioClient import os client = SimStudioClient(api_key=os.getenv("SIM_API_KEY")) def execute_with_streaming(): """Execute workflow with streaming enabled.""" try: # Enable streaming for specific block outputs result = client.execute_workflow( "workflow-id", input={"message": "Count to five"}, stream=True, selected_outputs=["agent1.content"] # Use blockName.attribute format ) print("Workflow result:", result) except Exception as error: print("Error:", error) execute_with_streaming() ``` The streaming response follows the Server-Sent Events (SSE) format: ``` data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"} data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"} data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}} data: [DONE] ``` **Flask Streaming Example:** ```python from flask import Flask, Response, stream_with_context import requests import json import os app = Flask(__name__) @app.route('/stream-workflow') def stream_workflow(): """Stream workflow execution to the client.""" def generate(): response = requests.post( 'https://sim.ai/api/v2/workflows/WORKFLOW_ID/execute', headers={ 'Content-Type': 'application/json', 'X-API-Key': os.getenv('SIM_API_KEY') }, json={ 'input': {'message': 'Generate a story'}, 'stream': True, 'selectedOutputs': ['agent1.content'] }, stream=True ) for line in response.iter_lines(): if line: decoded_line = line.decode('utf-8') if decoded_line.startswith('data: '): data = decoded_line[6:] # Remove 'data: ' prefix if data == '[DONE]': break try: parsed = json.loads(data) if 'chunk' in parsed: yield f"data: {json.dumps(parsed)}\n\n" elif parsed.get('event') == 'done': yield f"data: {json.dumps(parsed)}\n\n" print("Execution complete:", parsed.get('metadata')) except json.JSONDecodeError: pass return Response( stream_with_context(generate()), mimetype='text/event-stream' ) if __name__ == '__main__': app.run(debug=True) ``` ### Environment Configuration [#environment-configuration] Configure the client using environment variables: ```python import os from simstudio import SimStudioClient # Development configuration client = SimStudioClient( api_key=os.getenv("SIM_API_KEY"), base_url=os.getenv("SIM_BASE_URL", "https://sim.ai") ) ``` ```python import os from simstudio import SimStudioClient # Production configuration with error handling api_key = os.getenv("SIM_API_KEY") if not api_key: raise ValueError("SIM_API_KEY environment variable is required") client = SimStudioClient( api_key=api_key, base_url=os.getenv("SIM_BASE_URL", "https://sim.ai") ) ``` ## Getting Your API Key [#getting-your-api-key] Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. See [Authentication](/api-reference/authentication) for key types and permissions. Deploy the workflow before calling it through the SDK. Keep the key in a server-side environment variable. ## Requirements [#requirements] * Python 3.8+ * requests >= 2.25.0 ## License [#license] Apache-2.0 --- # TypeScript (/api-reference/typescript) Use the TypeScript SDK to execute workflows from server-side JavaScript or TypeScript. For browser applications, call the SDK through your authenticated backend. The TypeScript SDK provides full type safety, async execution support, retry helpers with exponential backoff, and usage tracking. ## Installation [#installation] Install the SDK using your preferred package manager: ```bash npm install simstudio-ts-sdk ``` ```bash yarn add simstudio-ts-sdk ``` ```bash bun add simstudio-ts-sdk ``` ## Quick Start [#quick-start] Here's a simple example to get you started: ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; // Initialize the client const client = new SimStudioClient({ apiKey: 'your-api-key-here', baseUrl: 'https://sim.ai' // optional, defaults to https://sim.ai }); // Execute a workflow try { const result = await client.executeWorkflow('workflow-id'); console.log('Workflow executed successfully:', result); } catch (error) { console.error('Workflow execution failed:', error); } ``` ## API Reference [#api-reference] ### SimStudioClient [#simstudioclient] #### Constructor [#constructor] ```typescript new SimStudioClient(config: SimStudioConfig) ``` **Configuration:** * `config.apiKey` (string): Your Sim API key * `config.baseUrl` (string, optional): Base URL for the Sim API (defaults to `https://sim.ai`) #### Methods [#methods] ##### executeWorkflow() [#executeworkflow] Execute a workflow with optional input data. ```typescript const result = await client.executeWorkflow('workflow-id', { message: 'Hello, world!' }, { timeout: 30000 // 30 seconds }); ``` **Parameters:** * `workflowId` (string): The ID of the workflow to execute * `input` (any, optional): Input data to pass to the workflow * `options` (ExecutionOptions, optional): * `timeout` (number): Timeout in milliseconds (default: 30000) * `stream` (boolean): Enable streaming responses (default: false) * `selectedOutputs` (string\[]): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`) * `async` (boolean): Execute asynchronously (default: false) * `executionTimeoutSeconds` (number): Optional server-side async execution cap from 1 to 604800 seconds. Requires `async: true` and cannot extend the account policy. **Returns:** `Promise` When `async: true`, returns immediately with a `runId` and `statusUrl` for polling. Otherwise, waits for completion. ##### getWorkflowStatus() [#getworkflowstatus] Get the status of a workflow (deployment status, etc.). ```typescript const status = await client.getWorkflowStatus('workflow-id'); console.log('Is deployed:', status.isDeployed); ``` **Parameters:** * `workflowId` (string): The ID of the workflow **Returns:** `Promise` ##### validateWorkflow() [#validateworkflow] Validate that a workflow is ready for execution. ```typescript const isReady = await client.validateWorkflow('workflow-id'); if (isReady) { // Workflow is deployed and ready } ``` **Parameters:** * `workflowId` (string): The ID of the workflow **Returns:** `Promise` ##### getWorkflowRun() [#getworkflowrun] Get the status and optional outputs of a workflow run. ```typescript const status = await client.getWorkflowRun('workflow-id', 'run-id', { includeOutput: true }); console.log('Status:', status.status); // 'queued', 'running', 'completed', 'failed' if (status.status === 'completed') { console.log('Output:', status.output); } ``` **Parameters:** * `workflowId` (string): The workflow ID * `runId` (string): The run ID returned from async execution * `options.includeOutput` (boolean, optional): Include the final output for completed executions * `options.selectedOutputs` (string\[], optional): Block output selectors to include **Returns:** `Promise` **Response fields:** * `runId` (string): The run ID * `workflowId` (string): The workflow ID * `status` (string): One of `'queued'`, `'pending'`, `'running'`, `'paused'`, `'completed'`, `'failed'`, `'cancelled'` * `startedAt` / `endedAt` (string): Execution timestamps * `durationMs` (number, nullable): Duration in milliseconds * `output` (any, nullable): The workflow output when requested for a completed execution * `blockOutputs` (object, nullable): Requested block outputs * `error` (object, nullable): Structured failure details with `code`, `message`, and optional `details` ##### getJobStatus() [#getjobstatus] Get the status of a job created through the legacy async execution endpoint. New integrations should use `getWorkflowRun()` with the run ID instead. ```typescript const status = await client.getJobStatus('legacy-job-id'); ``` ##### executeWithRetry() [#executewithretry] Execute a workflow with automatic retry on rate limit errors using exponential backoff. ```typescript const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, { timeout: 30000 }, { maxRetries: 3, // Maximum number of retries initialDelay: 1000, // Initial delay in ms (1 second) maxDelay: 30000, // Maximum delay in ms (30 seconds) backoffMultiplier: 2 // Exponential backoff multiplier }); ``` **Parameters:** * `workflowId` (string): The ID of the workflow to execute * `input` (any, optional): Input data to pass to the workflow * `options` (ExecutionOptions, optional): Same as `executeWorkflow()` * `retryOptions` (RetryOptions, optional): * `maxRetries` (number): Maximum number of retries (default: 3) * `initialDelay` (number): Initial delay in ms (default: 1000) * `maxDelay` (number): Maximum delay in ms (default: 30000) * `backoffMultiplier` (number): Backoff multiplier (default: 2) **Returns:** `Promise` The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a `retry-after` header, it will be used instead. ##### getRateLimitInfo() [#getratelimitinfo] Get the current rate limit information from the last API response. ```typescript const rateLimitInfo = client.getRateLimitInfo(); if (rateLimitInfo) { console.log('Limit:', rateLimitInfo.limit); console.log('Remaining:', rateLimitInfo.remaining); console.log('Reset:', new Date(rateLimitInfo.reset * 1000)); } ``` **Returns:** `RateLimitInfo | null` ##### getUsageLimits() [#getusagelimits] Get current usage limits and quota information for your account. ```typescript const limits = await client.getUsageLimits(); console.log('Sync requests remaining:', limits.rateLimit.sync.remaining); console.log('Async requests remaining:', limits.rateLimit.async.remaining); console.log('Current period cost:', limits.usage.currentPeriodCost); console.log('Plan:', limits.usage.plan); ``` **Returns:** `Promise` **Response structure:** ```typescript { success: boolean rateLimit: { sync: { isLimited: boolean limit: number remaining: number resetAt: string } async: { isLimited: boolean limit: number remaining: number resetAt: string } authType: string // 'api' or 'manual' } usage: { currentPeriodCost: number limit: number plan: string // e.g., 'free', 'pro' } } ``` ##### setApiKey() [#setapikey] Update the API key. ```typescript client.setApiKey('new-api-key'); ``` ##### setBaseUrl() [#setbaseurl] Update the base URL. ```typescript client.setBaseUrl('https://my-custom-domain.com'); ``` ## Types [#types] ### WorkflowExecutionResult [#workflowexecutionresult] ```typescript interface WorkflowExecutionResult { success: boolean; output?: any; error?: string; logs?: any[]; metadata?: { duration?: number; runId?: string; [key: string]: any; }; traceSpans?: any[]; totalDuration?: number; } ``` ### AsyncExecutionResult [#asyncexecutionresult] ```typescript interface AsyncExecutionResult { success: boolean; runId: string; statusUrl: string; message: string; async: true; } ``` ### WorkflowStatus [#workflowstatus] ```typescript interface WorkflowStatus { isDeployed: boolean; deployedAt?: string; needsRedeployment: boolean; } ``` ### RateLimitInfo [#ratelimitinfo] ```typescript interface RateLimitInfo { limit: number; remaining: number; reset: number; retryAfter?: number; } ``` ### UsageLimits [#usagelimits] ```typescript interface UsageLimits { success: boolean; rateLimit: { sync: { isLimited: boolean; limit: number; remaining: number; resetAt: string; }; async: { isLimited: boolean; limit: number; remaining: number; resetAt: string; }; authType: string; }; usage: { currentPeriodCost: number; limit: number; plan: string; }; } ``` ### SimStudioError [#simstudioerror] ```typescript class SimStudioError extends Error { code?: string; status?: number; } ``` **Common error codes:** * `UNAUTHORIZED`: Invalid API key * `TIMEOUT`: Request timed out * `RATE_LIMIT_EXCEEDED`: Rate limit exceeded * `USAGE_LIMIT_EXCEEDED`: Usage limit exceeded * `EXECUTION_ERROR`: Workflow execution failed ## Examples [#examples] ### Basic Workflow Execution [#basic-workflow-execution] Set up the SimStudioClient with your API key. Check if the workflow is deployed and ready for execution. Run the workflow with your input data. Process the execution result and handle any errors. ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function runWorkflow() { try { // Check if workflow is ready const isReady = await client.validateWorkflow('my-workflow-id'); if (!isReady) { throw new Error('Workflow is not deployed or ready'); } // Execute the workflow const result = await client.executeWorkflow('my-workflow-id', { message: 'Process this data', userId: '12345' }); if (result.success) { console.log('Output:', result.output); console.log('Duration:', result.metadata?.duration); } else { console.error('Workflow failed:', result.error); } } catch (error) { console.error('Error:', error); } } runWorkflow(); ``` ### Error Handling [#error-handling] Handle different types of errors that may occur during workflow execution: ```typescript import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function executeWithErrorHandling() { try { const result = await client.executeWorkflow('workflow-id'); return result; } catch (error) { if (error instanceof SimStudioError) { switch (error.code) { case 'UNAUTHORIZED': console.error('Invalid API key'); break; case 'TIMEOUT': console.error('Workflow execution timed out'); break; case 'USAGE_LIMIT_EXCEEDED': console.error('Usage limit exceeded'); break; case 'INVALID_JSON': console.error('Invalid JSON in request body'); break; default: console.error('Workflow error:', error.message); } } else { console.error('Unexpected error:', error); } throw error; } } ``` ### Environment Configuration [#environment-configuration] Configure the client using environment variables: ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; // Development configuration const apiKey = process.env.SIM_API_KEY; if (!apiKey) { throw new Error('SIM_API_KEY environment variable is required'); } const client = new SimStudioClient({ apiKey, baseUrl: process.env.SIM_BASE_URL // optional }); ``` ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; // Production configuration with validation const apiKey = process.env.SIM_API_KEY; if (!apiKey) { throw new Error('SIM_API_KEY environment variable is required'); } const client = new SimStudioClient({ apiKey, baseUrl: process.env.SIM_BASE_URL || 'https://sim.ai' }); ``` ### Node.js Express Integration [#nodejs-express-integration] Integrate with an Express.js server: ```typescript import express from 'express'; import { SimStudioClient } from 'simstudio-ts-sdk'; const app = express(); const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); app.use(express.json()); app.post('/execute-workflow', async (req, res) => { try { const { workflowId, input } = req.body; const result = await client.executeWorkflow(workflowId, input, { timeout: 60000 }); res.json({ success: true, data: result }); } catch (error) { console.error('Workflow execution error:', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); } }); app.listen(3000, () => { console.log('Server running on port 3000'); }); ``` ### Next.js API Route [#nextjs-api-route] Use with Next.js API routes: ```typescript // pages/api/workflow.ts import { NextApiRequest, NextApiResponse } from 'next'; import { SimStudioClient } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const { workflowId, input } = req.body; const result = await client.executeWorkflow(workflowId, input, { timeout: 30000 }); res.status(200).json(result); } catch (error) { console.error('Error executing workflow:', error); res.status(500).json({ error: 'Failed to execute workflow' }); } } ``` ### Browser Usage [#browser-usage] Keep the Sim API key on your server. A browser application should call an authenticated backend endpoint that checks which workflow the user can run, then calls the SDK. The [Next.js API Route](#nextjs-api-route) example shows where the server-side SDK call belongs; add your application authentication and authorization before executing a workflow. ### File Upload [#file-upload] File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format. The SDK converts File objects to this format: ```typescript { type: 'file', data: 'data:mime/type;base64,base64data', name: 'filename', mime: 'mime/type' } ``` Alternatively, you can manually provide files using the URL format: ```typescript { type: 'url', data: 'https://example.com/file.pdf', name: 'file.pdf', mime: 'application/pdf' } ``` Run file uploads on your backend after authenticating the caller and validating the upload. For example, in Node.js: ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; import { readFile } from 'node:fs/promises'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); const fileBuffer = await readFile('./document.pdf'); const file = new File([fileBuffer], 'document.pdf', { type: 'application/pdf' }); const result = await client.executeWorkflow('workflow-id', { documents: [file], query: 'Summarize this document' }); ``` ### Async Workflow Execution [#async-workflow-execution] Execute workflows asynchronously for long-running tasks: ```typescript import { SimStudioClient, AsyncExecutionResult } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function executeAsync() { try { // Start async execution const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, { async: true // Execute asynchronously }); // Check if result is an async execution if ('async' in result && result.async) { console.log('Run ID:', result.runId); console.log('Status endpoint:', result.statusUrl); // Poll for completion let status = await client.getWorkflowRun('workflow-id', result.runId, { includeOutput: true }); while (status.status === 'queued' || status.status === 'pending' || status.status === 'running') { console.log('Current status:', status.status); await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds status = await client.getWorkflowRun('workflow-id', result.runId, { includeOutput: true }); } if (status.status === 'completed') { console.log('Workflow completed!'); console.log('Output:', status.output); console.log('Duration:', status.durationMs); } else if (status.status === 'paused') { console.log('Workflow is paused and waiting for input or resumption.'); } else if (status.status === 'cancelled') { console.log('Workflow was cancelled.'); } else { console.error('Workflow failed:', status.error); } } } catch (error) { console.error('Error:', error); } } executeAsync(); ``` ### Rate Limiting and Retry [#rate-limiting-and-retry] Handle rate limits automatically with exponential backoff: ```typescript import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function executeWithRetryHandling() { try { // Automatically retries on rate limit const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, {}, { maxRetries: 5, initialDelay: 1000, maxDelay: 60000, backoffMultiplier: 2 }); console.log('Success:', result); } catch (error) { if (error instanceof SimStudioError && error.code === 'RATE_LIMIT_EXCEEDED') { console.error('Rate limit exceeded after all retries'); // Check rate limit info const rateLimitInfo = client.getRateLimitInfo(); if (rateLimitInfo) { console.log('Rate limit resets at:', new Date(rateLimitInfo.reset * 1000)); } } } } ``` ### Usage Monitoring [#usage-monitoring] Monitor your account usage and limits: ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function checkUsage() { try { const limits = await client.getUsageLimits(); console.log('=== Rate Limits ==='); console.log('Sync requests:'); console.log(' Limit:', limits.rateLimit.sync.limit); console.log(' Remaining:', limits.rateLimit.sync.remaining); console.log(' Resets at:', limits.rateLimit.sync.resetAt); console.log(' Is limited:', limits.rateLimit.sync.isLimited); console.log('\nAsync requests:'); console.log(' Limit:', limits.rateLimit.async.limit); console.log(' Remaining:', limits.rateLimit.async.remaining); console.log(' Resets at:', limits.rateLimit.async.resetAt); console.log(' Is limited:', limits.rateLimit.async.isLimited); console.log('\n=== Usage ==='); console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2)); console.log('Limit: $' + limits.usage.limit.toFixed(2)); console.log('Plan:', limits.usage.plan); const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100; console.log('Usage: ' + percentUsed.toFixed(1) + '%'); if (percentUsed > 80) { console.warn('⚠️ Warning: You are approaching your usage limit!'); } } catch (error) { console.error('Error checking usage:', error); } } checkUsage(); ``` ### Streaming Workflow Execution [#streaming-workflow-execution] Execute workflows with real-time streaming responses: ```typescript import { SimStudioClient } from 'simstudio-ts-sdk'; const client = new SimStudioClient({ apiKey: process.env.SIM_API_KEY! }); async function executeWithStreaming() { try { // Enable streaming for specific block outputs const result = await client.executeWorkflow('workflow-id', { message: 'Count to five' }, { stream: true, selectedOutputs: ['agent1.content'] // Use blockName.attribute format }); console.log('Workflow result:', result); } catch (error) { console.error('Error:', error); } } ``` The streaming response follows the Server-Sent Events (SSE) format: ``` data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"} data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"} data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}} data: [DONE] ``` For browser streaming, have your authenticated backend forward the Sim SSE response. Keep the Sim API key on the backend, check the upstream response status, and parse complete SSE events across network chunks. See [streaming responses](/workflows/deployment/api#streaming) for the event format. ## Getting Your API Key [#getting-your-api-key] Create a key in **Account settings → Sim API keys**, or use a workspace key if your administrator requires one. See [Authentication](/api-reference/authentication) for key types and permissions. Deploy the workflow before calling it through the SDK. Keep the key in a server-side environment variable. ## Requirements [#requirements] * Node.js 16+ * TypeScript 5.0+ (for TypeScript projects) ## License [#license] Apache-2.0 --- # Sim Desktop (/desktop) Sim Desktop is the macOS app for your Sim workspace. Everything the web app does, it does — plus the things a browser tab cannot: a built-in browser and terminal, read-only access to folders you pick, and native notifications when a task finishes. Sim Desktop — Chat on the left, the built-in browser on the right ## Download [#download] **[Download Sim Desktop for macOS](https://sim.ai/api/desktop/update/download)** One universal build runs natively on both Apple Silicon and Intel Macs. It is signed and notarized by Sim, so Gatekeeper accepts it with no override. That link is never version-pinned — it is a redirect resolved at request time, so it always lands on the newest release. Bookmark it, share it, or put it in an MDM policy; it stays correct. It resolves against sim.ai. Every deployment serves the same endpoint on its own origin, so if you self-host use yours — `https://sim.example.com/api/desktop/update/download` — to get the build your deployment offers. To install a specific version instead of the newest one, pick it from [the releases page](https://github.com/simstudioai/sim/releases) and download that release's `Sim--universal.dmg`. ## Install [#install] ### Open the disk image and drag Sim to Applications [#open-the-disk-image-and-drag-sim-to-applications] Install to `/Applications`. macOS App Translocation runs an app from a randomized read-only path when it is launched from Downloads, which silently breaks auto-updates. ### Sign in [#sign-in] Launch Sim and sign in as you normally would. Google, Microsoft, and SSO sign-ins finish in your default browser — those providers refuse to render inside an embedded browser. Sim opens the page, you approve, and the browser hands the session back to the app. The app gets its own session, so signing out of one surface does not sign out the other. ### Point it at your deployment, if you self-host [#point-it-at-your-deployment-if-you-self-host] Fresh installs open sim.ai. To use your own deployment, choose **Sim → Server…** in the menu bar and enter its URL. See [Desktop App on Your Deployment](/platform/self-hosting/desktop) for what changes when you switch. ## What the desktop app adds [#what-the-desktop-app-adds] * **A built-in browser.** A real browser inside the app, with its own tabs, saved passwords, and sessions. Chat can drive it — sign in once and your agents work on the sites you are already signed into. * **A built-in terminal.** Real shell sessions in a panel next to Chat, with tmux and shell integration, that Chat can run commands in. * **Local folder access.** When a task needs a folder on your Mac, Chat offers to open the native folder picker. The grant is read-only, scoped to the folder you picked, and revocable. * **Notifications.** A native notification when a Chat task finishes. Clicking it opens that chat. Tasks that end in an error, or that have another message queued behind them, do not notify. * **Control Center.** A menu-bar icon with your recent chats, so Sim is one click away from any app. * **Launch at login.** Sim starts with your Mac, and however you launch it, it opens where you left off. Both the browser and the terminal are capabilities you grant, not defaults you are stuck with — each has a single switch in settings that turns it off entirely. **What a folder grant does and does not do.** Granting a folder does not copy or upload it, and an agent cannot attach or stage a file from it — that stays your deliberate act. But when an agent reads or searches inside the grant, what it reads is a tool result, and tool results go to your Sim server and to the model like anything else in the conversation. The file stays on your Mac; what an agent reads out of it does not. ## Keyboard shortcuts [#keyboard-shortcuts] These are the app's own shortcuts. The [workflow editor and table shortcuts](/keyboard-shortcuts) work the same in the app as in the browser. | Shortcut | Action | | ----------------------- | --------------------------------- | | `Cmd` + `K` | Search | | `Cmd` + `B` | Toggle the sidebar | | `Cmd` + `N` | New chat | | `Cmd` + `Shift` + `N` | New window | | `Cmd` + `,` | Settings | | `Cmd` + `[` | Back | | `Cmd` + `R` | Reload | | `Cmd` + `0` / `+` / `-` | Reset, increase, or decrease zoom | With the built-in browser or terminal focused, the tab shortcuts act on its tabs rather than on the window: | Shortcut | Action | | ----------------------------------------- | ------------------------------------ | | `Cmd` + `T` | New tab | | `Cmd` + `W` | Close tab | | `Cmd` + `Shift` + `T` | Reopen the last closed tab | | `Ctrl` + `Tab` / `Ctrl` + `Shift` + `Tab` | Next / previous tab | | `Cmd` + `1`–`8` | Jump to that tab | | `Cmd` + `9` | Jump to the last tab | | `Cmd` + `L` | Focus the address bar (browser only) | | `Cmd` + `F` | Find on the page (browser only) | ## Settings [#settings] The desktop app adds three sections under **Settings → Account**. They appear only when you are running the app, and they apply to this Mac rather than to your account. ### Desktop [#desktop] * **Launch Sim at login** * **Show Sim in Control Center** — the menu-bar icon * **Automatically download updates** * **Enable desktop notifications**, with **Play notification sounds** and **Notify only when Sim isn't focused** It also shows the installed version, and the version waiting to be applied when an update is ready. ### Browser [#browser] * **Let Chat browse the web** — the master switch for the built-in browser * **Search suggestions**, **Theme**, **Default zoom**, and **Download location** * **Browsing data** — clear cookies, site data, and cached images and files ### Terminal [#terminal] * **Let Chat run commands** — the master switch for the built-in terminal * **Theme** and **Default zoom** The menu bar carries the rest: **Sim → Settings…** (`Cmd` + `,`), **Server…** to change deployments, **Check for Updates…**, and **Sign Out**. ## Updates [#updates] Sim checks the deployment it is pointed at rather than a global feed. How it applies what it finds depends on how the app was installed. Nothing is ever forced mid-session either way. **Installed in `/Applications`, signed by Sim** — what the download link above gives you. The app replaces itself. With **Automatically download updates** on, it downloads in the background and offers to restart; choose **Later** and the update applies the next time you quit. With it off, nothing downloads until you ask: **Sim → Check for Updates…** reports the available version and waits for you to choose **Download**. **Anywhere else** — outside `/Applications`, or a build not signed with a Developer ID. The app cannot replace itself, so it offers you the installer to download and swap in by hand. Updates come from the deployment you are connected to, so a self-hosted install controls which build its own users are offered. That control depends on the feed staying reachable: if it is not, a self-updating stable build falls back to Sim's public GitHub releases rather than stalling. See [Desktop App on Your Deployment](/platform/self-hosting/desktop). ## Requirements [#requirements] * **macOS 12 Monterey or later**, on Apple Silicon or Intel. * **Outbound access to your Sim deployment**, and to `github.com`, which is where installers and updates are downloaded from. * **If you self-host**, your Sim server needs its own outbound access to both `api.github.com`, which is what resolves *which* release to offer, and `github.com`. An allowlist carrying only `github.com` leaves the download endpoint answering `502`. * **A system-trusted TLS certificate**, if you self-host. The app rejects certificate errors outright and offers no override, so a private CA must be installed in the macOS keychain. The desktop app is macOS-only today. The web app works in any browser on any platform, and your account, workspaces, and workflows are the same either way. --- # Editor (/files/editor) Every markdown file in your workspace opens in a **rich editor**. Type markdown and it renders as you go, or format visually with the toolbar and slash menu. The file is saved as plain markdown.
A markdown file rendered in the editor: headings, bold, italic, and a link, a nested bullet list, and a table
## Formatting text [#formatting-text] Select any text to bring up the formatting toolbar — bold, italic, strikethrough, inline code, and links. The same marks appear instantly as you type the markdown for them, like `**bold**` or `*italic*`. Links show a hover card so you can open, copy, edit, or remove them without hunting through the source. ## Structure [#structure] Headings, blockquotes, and dividers keep long documents scannable. Type `# ` through `###### ` for headings, `> ` for a quote, and `---` for a divider. ## Lists and checklists [#lists-and-checklists] Bullet, ordered, and nested lists all work, plus task lists you can tick right in the document. ## Tables [#tables] Insert a table from the slash menu, then click any cell for the floating table toolbar — add or remove rows and columns, toggle the header row, or delete the table. Drag a column border to resize it. ## Code blocks [#code-blocks] Fenced code blocks are syntax-highlighted, with a language picker in the corner. Pick `mermaid` to render a live diagram instead of code. ## Images [#images] Paste or drag an image straight into the document, then drag a corner to resize it. ## Slash menu and shortcuts [#slash-menu-and-shortcuts] Type `/` anywhere to insert any block — heading, list, table, code block, image, and more — without leaving the keyboard. Familiar shortcuts work too: **Cmd/Ctrl + B** for bold, **Cmd/Ctrl + I** for italic, and **Cmd/Ctrl + K** to add a link over selected text. ## Markdown fidelity [#markdown-fidelity] The editor round-trips your markdown exactly — it saves what you wrote, with no reformatting churn. A few constructs can't be represented visually without losing information on save — footnotes, raw HTML, and HTML comments. When a file contains one of these, it opens **read-only** so the original source is preserved untouched. Everything is still rendered faithfully; you just can't edit that file inline. --- # Generating files (/files/generating) A generated file is an artifact a workflow run creates: a report, a CSV, a rendered audio clip. It starts as a value a block produces and becomes a workspace file when a [File](/integrations/file) block writes it to the [Files](/files) store. Once saved, it has a name, a size, and a URL, and any later run can read it back. {/* VISUAL: flow diagram: [Agent/Function block produces content] → [File block, Write] → [Files panel, file visible] */} ## What produces file content [#what-produces-file-content] Most generated files start as the output of an earlier block. Two patterns are common. A block returns **text content** you want to keep. An [Agent](/workflows/blocks/agent) writes an analysis or summary; a [Function](/workflows/blocks/function) builds a CSV or formats a report. That text is part of the block's output, read by reference as `` or ``. To make it a file, you pass that value into a File block set to Write. A block returns a **file object** directly. ElevenLabs text-to-speech returns an `audioFile`; an image generator returns a generated image; the File block's own Read operation returns parsed files. These are already [UserFile](/files/passing-files) objects (an object with `id`, `name`, `url`, `size`, and `type`), so they appear in the output panel as files with no Write step. You can hand one to a downstream block, or write its content to the Files store to persist it. A block's output is remembered under the block's name for the rest of the run, and a later block reads it by key, like `` or ``. Producing content and saving it are two separate steps: the producing block holds the value, the File block persists it. ## Saving content with the File block [#saving-content-with-the-file-block] The [File](/integrations/file) block's **Write** operation creates a new workspace file. It takes two required fields: a `fileName` (like `report.md`) and the `content` string to store. It returns the saved file's details. {/* VISUAL: File Write anatomy: inputs (fileName, content, contentType?) → outputs (id, name, size, url) */} To save an Agent's analysis, connect Agent into a File block, set the operation to Write, and reference the Agent's output: `fileName` = `research_summary.md`, `content` = ``. When the workflow runs, the File block writes the file and produces: * `id`: the canonical file ID, used to fetch the file later. * `name`: the final file name after any deduplication (see below). * `size`: the byte count. * `url`: an absolute URL to download or preview the file. The content type is detected from the file extension: `.csv` becomes `text/csv`, `.pdf` becomes `application/pdf`, `.md` becomes `text/markdown`. To set it yourself, fill in `contentType` in advanced mode. If a file with the same name already exists in the workspace, Write does not overwrite it. It appends a numeric suffix instead: a second `data.csv` is saved as `data (1).csv`, a third as `data (2).csv`. To add to an existing file rather than create a new one, use the **Append** operation, which writes content to the end of a named file. ## Where the file goes [#where-the-file-goes] A saved file lands in the workspace [Files](/files) store, the same place uploads live. It shows up in the Files panel in the sidebar with its name, size, type icon, and modified date, grouped by category: document, image, audio, video, or code. From there you can preview it, rename it, move it into a folder, or download it by URL. {/* VISUAL: Files panel UI: generated files listed with name, size, type icon, owner, modified date, folder */} Files are scoped to the workspace, not to one workflow. A file written by one workflow is visible to every other workflow in the same workspace. Any later run can read it back with the File block's Read or Get operation, or by selecting it in a file picker. During a run, the file also appears in the output panel as the File block's output, shown as a structured object with its `id`, `name`, `url`, and `size`. The output panel shows you one run as it happens, while the Files store is where the file stays afterward. {/* VISUAL: output panel tree: file object (id, name, url, size, type) during a run, beside the same file in the Files store */} ## Returning a generated file from a deployment [#returning-a-generated-file-from-a-deployment] When a workflow is deployed as an [API](/workflows/deployment/api), a generated file can be part of the response. Reference the file in a [Response](/workflows/blocks/response) block, or include its ID in the object you return. The caller uses the `url` or `id` to fetch the file from the workspace store. The file itself stays in the Files store, and the response carries a pointer to it, not the bytes. --- # Files (/files) A **file** is a document, image, spreadsheet, or PDF in your workspace. Files are how documents and media move into and out of your agents. Your team uploads them, you create them in the editor, or a workflow produces them, and they all live in one store shared across the workspace. Any [workflow](/workflows) can read a file or produce one. You might upload a contract for an agent to review, generate a report from a table, or hand a workflow the document it needs to answer a question. ## How files fit the workspace [#how-files-fit-the-workspace] * **[Workflows](/workflows)** read files, like a PDF to summarize, and produce them, like a rendered report. See [using files in workflows](/files/using-in-workflows). * **[Knowledge bases](/knowledgebase)** are built from files you upload, turning their contents into searchable memory. * **[Deployments](/workflows/deployment)** can take a file as input and return one as output. Use a file when the document or media itself is what matters. Use a [table](/tables) when you need structured rows and fields, and a [knowledge base](/knowledgebase) when an agent needs to search across many documents. --- # Passing files (/files/passing-files) A file moves through a workflow as a standardized **file object**. Blocks receive it, act on it, and pass it on — this page covers the object's shape, how to reference it between blocks, and how files enter and leave through the API. ## File Objects [#file-objects] When blocks output files (like Gmail attachments, generated images, or parsed documents), they return a standardized file object: ```json { "id": "f_8c2...", "name": "report.pdf", "url": "https://...", "size": 245678, "type": "application/pdf", "base64": "JVBERi0xLjQK..." } ``` You can access any of these properties when referencing files from previous blocks. ## The File Block [#the-file-block] The **File block** brings a file into a workflow. It accepts files from any source and outputs standardized file objects every block understands. **Inputs:** * **Uploaded files** - Drag and drop or select files directly * **External URLs** - Any publicly accessible file URL * **Files from other blocks** - Pass files from Gmail attachments, Slack downloads, etc. **Outputs:** * A list of `UserFile` objects with consistent structure (`id`, `name`, `url`, `size`, `type`, `base64`) * `contents` - Extracted text per file (the Get Content operation) * `combinedContent` - All fetched files' text merged into one string (the Fetch operation) **Example usage:** ``` // Get all files from the File block // Get the first file // Get the first file's extracted text (Get Content operation) ``` The File block automatically: * Detects file types from URLs and extensions * Extracts text from PDFs, CSVs, and documents * Generates base64 encoding for binary files * Creates presigned URLs for secure access Use the File block when you need to normalize files from different sources before passing them to other blocks like Vision, STT, or email integrations. ## Passing Files Between Blocks [#passing-files-between-blocks] Reference files from previous blocks using the tag dropdown. Click in any file input field and type `<` to see available outputs. **Common patterns:** ``` // Single file from a block // Pass the whole file object // Access specific properties ``` Most blocks accept the full file object and extract what they need automatically. You don't need to manually extract `base64` or `url` in most cases. ## Triggering Workflows with Files [#triggering-workflows-with-files] When calling a workflow via API that expects file input, include files in your request: ```bash curl -X POST "https://sim.ai/api/v2/workflows/YOUR_WORKFLOW_ID/execute" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "document": { "name": "report.pdf", "base64": "JVBERi0xLjQK...", "type": "application/pdf" } }' ``` ```bash curl -X POST "https://sim.ai/api/v2/workflows/YOUR_WORKFLOW_ID/execute" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "document": { "name": "report.pdf", "url": "https://example.com/report.pdf", "type": "application/pdf" } }' ``` The workflow's Start block should have an input field configured to receive the file parameter. ## Receiving Files in API Responses [#receiving-files-in-api-responses] When a workflow outputs files, they're included in the response: ```json { "success": true, "output": { "generatedFile": { "name": "output.png", "url": "https://...", "base64": "iVBORw0KGgo...", "type": "image/png", "size": 34567 } } } ``` Use `url` for direct downloads or `base64` for inline processing. ## Blocks That Work with Files [#blocks-that-work-with-files] **File inputs:** * **File** - Parse documents, images, and text files * **Agent** - Read images with a vision-capable model, or documents as text * **Mistral Parser** - Extract text from PDFs **File outputs:** * **Gmail** - Email attachments * **Slack** - Downloaded files * **TTS** - Generated audio files * **Video Generator** - Generated videos * **Image Generator** - Generated images **File storage:** * **Supabase** - Upload/download from storage * **S3** - AWS S3 operations * **Google Drive** - Drive file operations * **Dropbox** - Dropbox file operations Files are automatically available to downstream blocks. The engine handles all file transfer and format conversion. ## Best Practices [#best-practices] 1. **Use file objects directly** - Pass the full file object rather than extracting individual properties. Blocks handle the conversion automatically. 2. **Check file types** - Ensure the file type matches what the receiving block expects. An Agent using a vision-capable model can read images, while the File block handles documents. 3. **Consider file size** - Large files increase run time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage. --- # Using files in workflows (/files/using-in-workflows) A file is a document, image, spreadsheet, or PDF in your workspace. A workflow can read a file to act on its contents, pass a file to a block or tool that needs one (attach a PDF to an email, send an image to a vision model), or produce a new file and save it. The [File](/integrations/file) block is how a file enters or leaves a workflow; the work in between is done by whatever block the task calls for. What you do with a file depends on the task, so this page covers the File block's operations and how a file moves between blocks rather than one fixed recipe. The example we'll use throughout reads `report.pdf`, asks an agent to summarize it, and saves the summary as `summary.md`, which exercises reading, processing, and writing in one workflow. ## The File block [#the-file-block] The **File** block is one block with five operations, chosen from a dropdown. Each operation is a different way a file, or its contents, enters or leaves the workflow. | Operation | What it does | Outputs | | --------------- | -------------------------------------------------------------- | --------------------------- | | **Read** | Take an existing workspace file, by file picker or by file ID. | `files` | | **Get Content** | Extract a workspace file's text, by file picker or by file ID. | `contents` | | **Fetch** | Download and parse a file from an external URL. | `files`, `combinedContent` | | **Write** | Create a new workspace file from a name and text content. | `id`, `name`, `size`, `url` | | **Append** | Add text to the end of an existing workspace file. | `id`, `name`, `size`, `url` | Read hands the next block the **file itself**; Get Content hands it the **text inside**. Fetch brings in both for an external URL. Write and Append put a file out. A workflow uses only the operations its task needs, often just Read. The two sections below cover Read and Write; the other operations are variants noted alongside. {/* VISUAL: File block config UI. Operation dropdown open showing Read / Get Content / Fetch / Write / Append */} ## Reading a file in [#reading-a-file-in] In our example, the first File block is set to **Read**, with `report.pdf` chosen from the file picker. When it runs, it produces `files`: a list of **file objects**, one per file read. The first is ``. When the next step needs the file's *text* rather than the file itself, use **Get Content** instead: it extracts the text and outputs `contents`, an array with one string per file, read as ``. A **file object** is the standard shape Sim uses for every file. It carries the file's details: ```jsonc { "id": "wf_V1StGXR8z5jdHi6B…", // workspace file ID "name": "report.pdf", "url": "https://…", // where to access it "size": 248120, // bytes "type": "application/pdf" } ``` You rarely type a reference by hand. Wherever a block parameter accepts a file or a value, the builder lists the available outputs and you pick the one you want. Read mode also takes a file ID directly in advanced mode, which is how you read a file produced earlier in the same run. **Fetch** brings in files that live outside the workspace. Point it at a URL, and add request headers (such as `Authorization: Bearer …`) when the download needs authentication. It outputs `files` like Read does, plus `combinedContent`: the fetched files' text merged into one string. {/* VISUAL: File block in Read mode, file picker open with report.pdf selected; callout on the files[] output */} ## Processing the file [#processing-the-file] Once a file is read, a processing block reads that output by name. Two blocks do this, and they consume the file differently. ### Agent block [#agent-block] An [Agent](/workflows/blocks/agent) block has a **Files** input. Reference the read file there, ``, and write the instruction in the prompt: "Summarize this document." The agent receives the file object, not just its text, so a **vision-capable model** can analyze images and scanned pages directly. Other models work from the file's text content. In our example the Agent reads ``, summarizes it, and keeps the summary under its own name as ``. {/* VISUAL: Agent block config. Files input bound to , prompt "Summarize this document" */} ### Function block [#function-block] A [Function](/workflows/blocks/function) block runs code, and it usually works with file **text**. Read the file with **Get Content** and pass the extracted text, ``, and the code can parse, filter, or reshape it and return the result as its own output. A Function can also take the file object itself and read it in code with the `sim.files` helpers, like `await sim.files.readText(file)` — see [the Function block](/workflows/blocks/function) for those. Use a Function when the file is structured text (a CSV or JSON dump) and you want exact, deterministic processing instead of a model's interpretation. The two blocks differ in what they take. An **Agent** takes the file object on its Files input, while a **Function** typically takes the file's text from Get Content's `contents`. Both store their result under their own name for the next block to read. ## Writing a file out [#writing-a-file-out] Writing a file is optional, and many workflows skip it. The agent's summary could be returned in the response, posted to Slack, or emailed as-is without ever becoming a workspace file. Write a file when you specifically need a new one to keep, download, or hand to a later run. The last block in our example is a File block set to **Write**. Give it a file name and the content to save: * `fileName`: `summary.md` * `content`: `` Write creates a new workspace file and returns its `id`, `name`, `size`, and `url`. If a file with that name already exists, Write keeps both by adding a numeric suffix to the new one. The saved file lands in your workspace [files](/files), ready for the next run, a download, or another workflow. {/* VISUAL: run log showing the File Write step with the returned file id, name, and url */} **Append** adds to a file instead of replacing it. Target an existing workspace file by name and give it the content to add to the end. Use it to accumulate across runs, such as appending each run's observation to one `notes.md`. ## Composing the steps [#composing-the-steps] Each block references the previous one by name, so you compose only the steps a task needs. A contract read by an Agent that returns a verdict stops at processing, and an uploaded image described by a vision model never touches Write, while a fetched CSV cleaned by a Function and saved as a new file uses all three. Reading a file in is common, and writing one out is only for when the result is itself a file. For the file-object schema in full, base64 access, and how files move across API and chat triggers, see [Passing files](/files/passing-files). --- # Files & documents (/chat/files)