# Get agent details Source: https://docs.hellofriday.ai/api-reference/agents/get-agent-details /api-reference/openapi.json get /api/agents/{id} Get detailed information about a specific agent. # List agents Source: https://docs.hellofriday.ai/api-reference/agents/list-agents /api-reference/openapi.json get /api/agents List all available agents. # Delete artifact Source: https://docs.hellofriday.ai/api-reference/artifacts/delete-artifact /api-reference/openapi.json delete /api/artifacts/{id} Soft-delete an artifact. # Get artifact Source: https://docs.hellofriday.ai/api-reference/artifacts/get-artifact /api-reference/openapi.json get /api/artifacts/{id} Get an artifact by ID with inline contents or database preview. # List artifacts Source: https://docs.hellofriday.ai/api-reference/artifacts/list-artifacts /api-reference/openapi.json get /api/artifacts List artifacts with optional filtering by space or chat. # Upload file as artifact Source: https://docs.hellofriday.ai/api-reference/artifacts/upload-file-as-artifact /api-reference/openapi.json post /api/artifacts/upload Upload a file and convert it to an artifact. # Send a chat message Source: https://docs.hellofriday.ai/api-reference/chat/send-a-chat-message /api-reference/openapi.json post /api/workspaces/{workspaceId}/chat Send a message to a space and receive a streaming response via Server-Sent Events. # Get environment config Source: https://docs.hellofriday.ai/api-reference/configuration/get-environment-config /api-reference/openapi.json get /api/config/env Get environment variables from the daemon's configuration. # Update environment config Source: https://docs.hellofriday.ai/api-reference/configuration/update-environment-config /api-reference/openapi.json put /api/config/env Update environment variables in the daemon's configuration. # Get daemon status Source: https://docs.hellofriday.ai/api-reference/health/get-daemon-status /api-reference/openapi.json get /api/daemon/status Get detailed daemon status including memory usage and active spaces. # Health check Source: https://docs.hellofriday.ai/api-reference/health/health-check /api-reference/openapi.json get /health Check if the Friday daemon is running and get basic status information. # Cancel session Source: https://docs.hellofriday.ai/api-reference/sessions/cancel-session /api-reference/openapi.json delete /api/sessions/{id} Cancel a running session. # Get session Source: https://docs.hellofriday.ai/api-reference/sessions/get-session /api-reference/openapi.json get /api/sessions/{id} Get details of a specific session. # List sessions Source: https://docs.hellofriday.ai/api-reference/sessions/list-sessions /api-reference/openapi.json get /api/sessions List all session summaries, optionally filtered by space. # Trigger a signal Source: https://docs.hellofriday.ai/api-reference/signals/trigger-a-signal /api-reference/openapi.json post /api/workspaces/{workspaceId}/signals/{signalId} Manually trigger a signal in a space. Returns JSON by default, or an SSE stream if `Accept: text/event-stream` is set. # Create a space Source: https://docs.hellofriday.ai/api-reference/spaces/create-a-space /api-reference/openapi.json post /api/workspaces/create Create a new space from a configuration object. Creates the directory and files. # Delete a space Source: https://docs.hellofriday.ai/api-reference/spaces/delete-a-space /api-reference/openapi.json delete /api/workspaces/{workspaceId} Delete a registered space. # Get space details Source: https://docs.hellofriday.ai/api-reference/spaces/get-space-details /api-reference/openapi.json get /api/workspaces/{workspaceId} Get a space's details and configuration. # List spaces Source: https://docs.hellofriday.ai/api-reference/spaces/list-spaces /api-reference/openapi.json get /api/workspaces List all registered spaces sorted by name. # Register a space Source: https://docs.hellofriday.ai/api-reference/spaces/register-a-space /api-reference/openapi.json post /api/workspaces/add Register a space by providing the path to a directory containing a `workspace.yml` file. # Agents Source: https://docs.hellofriday.ai/core-concepts/agents Built-in and custom agents that execute operations in your agentic workflows. Agents are what do the actual work in your Friday jobs. There are three types you can declare in `workspace.yml`: `atlas` (built-in), `llm` (inline), and `user` (custom Python). ## atlas — built-in agents Built-in agents are the fastest path. Friday ships with a growing library of them for common integrations. Reference them by ID in your space: ```yaml workspace.yml theme={null} agents: gh: type: atlas agent: gh description: "Runs GitHub CLI operations — clone, review, post comments." prompt: "Execute GitHub CLI operation." env: GH_TOKEN: from_environment ``` The `prompt` field is per-invocation task context layered on the agent's built-in behavior — describe what you want, not how to do it. The `env` block passes credentials; use `from_environment` to pull from the daemon's environment. **Built-in agents are black-box.** They have their own internal tool surface and ignore the `tools` array. If you need to call specific MCP tools, use `type: llm` instead. Some available built-in agents: Agents that call an LLM use `ANTHROPIC_API_KEY` by default — `OPENAI_API_KEY`, `GEMINI_API_KEY`, and `GROQ_API_KEY` are also supported experimentally. The table lists only the **additional** credentials each agent needs beyond the LLM key. | Agent ID | What it does | Additional credentials | | ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `web` | Web research, browser automation, JS-rendered pages | Optional `PARALLEL_API_KEY` for search | | `slack` | Post messages to Slack channels and DMs | `SLACK_MCP_XOXP_TOKEN` | | `get-summary` | Condense long-form content into formatted summaries | None | | `claude-code` | Code generation, debugging, and codebase analysis in a sandboxed environment | Optional `GH_TOKEN` | | `gh` | GitHub CLI operations — clone repos, view PRs, fetch diffs, post reviews | `GH_TOKEN` | | `bb` | Bitbucket Cloud — clone repos, review PRs, post comments | `BITBUCKET_EMAIL` + `BITBUCKET_TOKEN` | | `jira` | Jira Cloud — read, create, update issues and transitions | `JIRA_EMAIL` + `JIRA_API_TOKEN` + `JIRA_SITE` | | `transcribe` | Transcribe audio files to text using Whisper | `GROQ_API_KEY` | | `hubspot` | HubSpot CRM — search, read, create, update contacts/deals/companies | `HUBSPOT_ACCESS_TOKEN` | | `image-generation` | Generate new images and edit existing image artifacts | `GEMINI_API_KEY` | | `knowledge-hybrid` | Hybrid RAG knowledge base search (BM25 + vector + reranker) | `KNOWLEDGE_CORPUS_PATH` + `FIREWORKS_API_KEY`, optional `GROQ_API_KEY` for reranking | This is a selection of available built-in agents — more are being added all the time. To browse the full list, open the [Agent Tester](/guides/agent-tester) in the Studio. ## llm — inline LLM agents Define an LLM agent directly in `workspace.yml` when no built-in agent covers your domain. Specify provider, model, system prompt, and the MCP tools it can call: ```yaml workspace.yml theme={null} agents: email-triage: type: llm description: "Classifies inbound email as urgent, tracking, or ignore." config: provider: anthropic model: claude-sonnet-4-6 prompt: | You triage inbound email. Classify each message as one of: urgent, tracking, ignore. Return JSON: { category, reason }. tools: - google-gmail/search_gmail_messages - google-gmail/get_gmail_message_content max_steps: 6 ``` Tool names use `serverId/toolName` format — the server ID comes from `tools.mcp.servers` in your space. Built-in platform tools (`memory_save`, `memory_read`) need no prefix. Use the [Agent Tester](/guides/agent-tester) to browse available tools. Supported providers: `anthropic`, `openai`, `google`, `groq`. Additional `llm` config fields: `temperature` (default `0.3`), `max_tokens`, `max_retries`, `timeout`, `tool_choice`, and `provider_options` for pass-through provider configuration. ## user — custom agents Build your own agent with the [Agent SDK](/sdk/overview) when you need to wrap internal APIs or business logic. Python is supported today; more languages are coming soon. Register the agent with the daemon, then reference it by ID: ```yaml workspace.yml theme={null} agents: my-agent: type: user description: "Wraps our internal data API." env: API_KEY: from_environment ``` Register with the daemon (no restart required): ```bash theme={null} friday agent register /abs/path/to/my-agent ``` See the [Agent SDK documentation](/sdk/overview) for how to write agents. Use the [Agent Tester](/guides/agent-tester) to test any agent in isolation before wiring it into a workflow. ## MCP tools Agents can call external tool servers via MCP. Declare servers at the space level — they're available to any `llm` or `user` agent in the space. See [MCP Tools](/core-concepts/mcp). ## Declaration order Declare agents **before** jobs in `workspace.yml`. Jobs reference agents by ID in FSM `entry` actions — the validator raises `unknown_agent_id` if the agent isn't declared first. **Let Friday write your agent.** Describe what you need in the Studio chat — Friday will scaffold a working agent, register it with the daemon, and wire it into your space. You can also install the [`writing-friday-python-agents`](https://github.com/friday-platform/agent-sdk/tree/main/packages/python/skills/writing-friday-python-agents) skill in Claude Code to author agents locally. # API Source: https://docs.hellofriday.ai/core-concepts/api The Friday daemon — the local HTTP backend that powers spaces, agents, sessions, and signals. The Friday daemon is the local backend service that runs spaces, orchestrates agents, manages sessions, and exposes an HTTP API for programmatic access. Everything you can do in Friday Studio — and more — is reachable through this API. ## How it runs The daemon ships with Friday Studio and starts automatically when you launch the app. The installed app serves the API at `https://local.hellofriday.ai:18080` — a loopback hostname that resolves to your machine, with a trusted TLS certificate so the browser and `curl` accept it without warnings. Because the daemon binds to your local machine, no authentication is required and traffic never leaves it — your spaces, prompts, and outputs stay local. Studio is a web UI that talks to this same API. The [CLI](/reference/cli) and SDK do too. That means anything you build against the API — scripts, automations, integrations — works the same way Studio does, against the same surface. ## Endpoint groups The daemon organizes endpoints into a small set of resource groups. Each group has its own reference page with request and response schemas: | Group | What it does | | -------------------------------------------------------------------- | ----------------------------------------------------------- | | [Health](/api-reference/health/health-check) | Daemon liveness and status — verify the daemon is reachable | | [Spaces](/api-reference/spaces/list-spaces) | Create, list, register, configure, and delete spaces | | [Chat](/api-reference/chat/send-a-chat-message) | Send a prompt to a space and stream the response | | [Sessions](/api-reference/sessions/list-sessions) | Inspect running and historical job sessions | | [Signals](/api-reference/signals/trigger-a-signal) | Trigger automations from your own code | | [Agents](/api-reference/agents/list-agents) | Discover the agents available to a space | | [Artifacts](/api-reference/artifacts/list-artifacts) | Retrieve files, reports, and data produced by agents | | [Configuration](/api-reference/configuration/get-environment-config) | Read and update daemon environment variables | ## Health check Use the health endpoint to confirm the daemon is up before issuing other requests: ```bash theme={null} curl https://local.hellofriday.ai:18080/health ``` A running daemon responds with a JSON object describing how many spaces are loaded, how long it's been up, and the runtime it's built on: ```json theme={null} { "activeWorkspaces": 3, "uptime": 3600000, "timestamp": "2026-03-24T10:00:00.000Z", "version": { "deno": "...", "v8": "...", "typescript": "..." } } ``` If the call fails, Friday Studio isn't running — launch it and retry. ## Streaming responses Chat and session endpoints support Server-Sent Events (SSE) so you can stream model output as it's generated. Add `-N` to `curl` (or set `Accept: text/event-stream` from your HTTP client) to keep the connection open and receive events as they arrive: ```bash theme={null} curl -N -X POST https://local.hellofriday.ai:18080/api/workspaces/{workspaceId}/chat \ -H 'Content-Type: application/json' \ -d '{ "id": "chat-001", "message": { "role": "user", "content": "What can you help me with?" } }' ``` The same pattern works for any endpoint that emits incremental progress — including signal triggers that you want to follow in real time via a `streamId`. ## Webhook tunnel To receive webhooks from external services like GitHub, Bitbucket, or Jira, the platform includes a webhook tunnel on port 19090 that exposes a public URL through Cloudflare. The tunnel forwards inbound requests to your local daemon, so HTTP signals work even when Friday is only running on your laptop. See [HTTP signals](/reference/signals/http) for tunnel setup, payload examples, and security guidance. ## Triggering signals from your code Signals are the canonical way to start jobs. Once a signal is defined in your `workspace.yml`, you can fire it from anywhere — a cron, another agent, a button in your own UI — by POSTing to its endpoint: ```bash theme={null} curl -X POST https://local.hellofriday.ai:18080/api/workspaces/{workspaceId}/signals/{signalId} \ -H 'Content-Type: application/json' \ -d '{ "payload": { "key": "value" } }' ``` The `payload` is passed through to the job. See [Signals](/core-concepts/signals) for the conceptual model and [Triggering signals manually](/core-concepts/signals#triggering-signals-manually) for CLI and Studio equivalents. ## When to use the API * **Build a custom integration** — embed Friday into your own product, dashboard, or workflow tool. * **Automate from CI or scripts** — kick off jobs from GitHub Actions, scheduled tasks, or your shell. * **Pipe outputs to other systems** — fetch artifacts and forward them to storage, chat, or downstream pipelines. * **Drive multiple spaces** — manage many spaces from a single script instead of the Studio UI. For day-to-day usage, Studio and the [CLI](/reference/cli) wrap these endpoints — reach for the API when you need to script, integrate, or automate. ## API reference For the full list of endpoints with request and response schemas, parameter details, and an in-browser **Try it** panel, see the [API reference](/api-reference/introduction). # Architecture Source: https://docs.hellofriday.ai/core-concepts/architecture High-level overview of the components that make up Friday. Friday runs as a set of local services managed by the Friday Studio launcher. Here's how they fit together. ## Daemon **Port 18080** — The core of Friday. The daemon is the backend service that runs everything. It manages space lifecycles, orchestrates agents, executes jobs, and exposes the HTTP API that all other components talk to. Key responsibilities: * **Space management** — loads `workspace.yml` configurations, creates runtime instances on demand, and tears them down after idle timeout * **Job engine** — executes jobs defined as finite state machines, routing between states and managing transitions * **Agent orchestration** — dispatches agents (built-in, custom Python, or inline LLM), manages MCP tool connections, and collects results * **Signal routing** — receives signals (HTTP webhooks, cron schedules, filesystem events, and chat messages from Slack, Discord, Telegram, and WhatsApp) and routes them to the correct space and job * **Streaming** — pushes real-time execution progress to connected clients via Server-Sent Events (SSE) * **Storage** — persists space registries, session history, artifacts, and activity logs using SQLite by default The daemon is the only component that other services need to reach. Everything else communicates through it. ## Studio **Port 15200** — The local web dashboard. The Studio is your visual interface for managing spaces, testing agents, inspecting job executions, and browsing skills. It communicates with the daemon API over HTTP. Key tools: * **Space dashboard** — view runs, jobs, signals, and agents for each loaded space * **[Agent Tester](/guides/agent-tester)** — test any agent in isolation with different inputs and models * **[Job Inspector](/guides/job-inspector)** — debug jobs with DAG visualization and waterfall timelines * **[Skills browser](/core-concepts/skills)** — navigate, edit, and publish skills * **Configuration editor** — edit `workspace.yml` with syntax highlighting ## Launcher The Friday Studio launcher is a macOS system tray app that manages the full service stack: daemon, Studio, webhook tunnel, Link, and NATS. It starts everything together, shows per-service health status, and shuts down cleanly. The launcher runs a health server at `http://127.0.0.1:5199`: * `GET /api/launcher-health` — JSON snapshot of per-service status * `GET /api/launcher-health/stream` — SSE stream of service status transitions * `POST /api/launcher-shutdown` — trigger orderly shutdown ## Link **Port 13100** — Credential management service. Link manages OAuth connections and API key storage. When you connect an integration (Google Calendar, Slack, GitHub, etc.), Link handles the OAuth flow and stores tokens locally. The daemon routes credential requests through Link at runtime. ## Chat interface The primary way to build and manage spaces is through the Friday chat interface. Open any space in Studio and use the **Chat** tab to talk to Friday — describe what you want, and Friday creates signals, agents, jobs, and space configuration for you. The chat interface is backed by the workspace-chat agent, which has access to the full space API — it can create spaces, add signals, wire up jobs, configure MCP servers, and deploy changes, all through conversation. ## Webhook Tunnel **Port 19090** (public-facing) · **Port 20241** (cloudflared transport, loopback) — Public URL for receiving external webhooks. The webhook tunnel creates a Cloudflare tunnel so external services (GitHub, Jira) can send webhooks to your local instance. Without it, you'd need to expose your machine to the internet manually. How it works: 1. On startup, the tunnel registers with Cloudflare and gets a public URL 2. External services send webhooks to `https://{tunnel-domain}/hook/{provider}/{workspaceId}/{signalId}` 3. The tunnel verifies the webhook signature (if configured) and forwards the payload to the daemon 4. The daemon routes it to the correct space and triggers the job Set `TUNNEL_TOKEN` for a stable URL across restarts, or let it generate a random temporary URL each time. Set `NO_TUNNEL=true` to disable it entirely. ## NATS **Port 4222** (protocol) · **Port 8222** (monitor / healthz) NATS is the internal message bus Friday uses for agent communication. The daemon, agents, and internal services communicate over NATS — you don't interact with it directly. The monitor endpoint at `http://localhost:8222/healthz` can be used to verify NATS is running. ## CLI **Binary: `friday`** — Command-line interface for the daemon. The `friday` CLI communicates with the daemon over HTTP (`localhost:18080`). It's an alternative to the Studio for managing spaces, triggering signals, inspecting sessions, and publishing skills. See the [CLI reference](/reference/cli) for the full command list. ## How they communicate | From | To | Purpose | | ----------------- | -------------- | -------------------------------------------- | | Studio | Daemon | All UI operations, real-time streaming | | CLI | Daemon | All CLI operations | | Webhook Tunnel | Daemon | Forward external webhooks | | External services | Webhook Tunnel | GitHub, Jira webhooks | | Agents | NATS | Internal message bus for agent communication | ## Data flow When you trigger a job, here's what happens: An external webhook, cron schedule, or manual trigger hits the daemon. The daemon finds the target space. If its runtime isn't loaded, it creates one on demand from the `workspace.yml`. A new session is created to track this execution. The job engine loads the job's FSM definition. The job engine walks through FSM states. At each action state, it dispatches agents via the orchestrator. Agents connect to MCP tool servers, call LLMs, and produce results. Each agent step, tool call, and state transition is pushed to connected clients via SSE. The Studio renders progress in real time. On completion (or failure), the session history and any produced artifacts are persisted to storage. # Communicators Source: https://docs.hellofriday.ai/core-concepts/communicators Connect Friday to chat platforms — Slack, Discord, Telegram, WhatsApp, Microsoft Teams. Communicators connect a Friday space to a chat platform so users can converse with it on the platform of their choice. The same agents, skills, and state that power Friday's web chat respond on Slack, Discord, Telegram, WhatsApp, or Microsoft Teams — you just plug in a bot. ## Supported platforms * **[Slack](/guides/communicators/slack)** — DMs and `@mentions` in channels * **[Discord](/guides/communicators/discord)** — DMs and `@mentions` in servers * **[Telegram](/guides/communicators/telegram)** — bot DMs * **[WhatsApp](/guides/communicators/whatsapp)** — WhatsApp Business chat * **[Microsoft Teams](/guides/communicators/teams)** — DMs and `@mentions` ## YAML Configuration Once connected, the space's `workspace.yml` records which communicators are active: ```yaml workspace.yml theme={null} communicators: slack: kind: slack ``` No secrets in the YAML — credentials are stored separately and scoped to the space. ## Inbound paths Slack, Telegram, WhatsApp, and Teams send events to your Friday instance over HTTPS. That means they need a publicly reachable URL to post to — Friday can't receive messages if it's only listening on `localhost`. Friday includes a built-in **webhook tunnel** that handles this automatically. When Friday Studio starts, the tunnel creates a public Cloudflare URL and routes incoming webhooks to your local daemon. You don't need to configure anything — the URL appears in Studio when you connect a communicator. Discord works differently: your Friday instance connects outward to Discord and holds the connection open, so no public URL is needed. | Platform | Public URL needed? | How it's handled | | -------- | ------------------ | -------------------------- | | Slack | Yes | Webhook tunnel (automatic) | | Telegram | Yes | Webhook tunnel (automatic) | | WhatsApp | Yes | Webhook tunnel (automatic) | | Teams | Yes | Webhook tunnel (automatic) | | Discord | No | Outbound connection | Each platform's setup page explains where to paste the tunnel URL when configuring the bot. The URL is shown in Studio when you connect the communicator. ## Learn more Connect a Slack app to a space for DMs and @mentions. Connect a Discord bot to a space for DMs and @mentions. Connect a Telegram bot to a space for direct messages. Connect a WhatsApp Business number to a space for customer chat. Connect a Microsoft Teams bot to a space for DMs and @mentions. # Jobs Source: https://docs.hellofriday.ai/core-concepts/jobs Jobs that orchestrate your agents step by step. Jobs orchestrate your agents. Each job is a finite state machine (FSM) — you define the states it moves through, what agents execute at each state, and how it transitions forward. ## Anatomy of a job ```yaml workspace.yml theme={null} jobs: review-pr: title: "Review PR" description: "Clones the repo, reviews the diff, posts inline comments." triggers: - signal: review-pr fsm: id: review-pr-pipeline initial: idle states: idle: on: review-pr: target: clone clone: entry: - type: agent agentId: gh outputTo: clone-output prompt: "Clone the repo for PR: {{payload.pr_url}}" - type: emit event: DONE on: DONE: target: review review: entry: - type: agent agentId: claude-code inputFrom: clone-output outputTo: review-output prompt: "Review the diff and return structured findings." - type: emit event: DONE on: DONE: target: completed completed: type: final config: timeout: "10m" max_steps: 20 ``` ## The trigger contract When a signal fires, the runtime resets the FSM to `initial` and sends `{ type: , data: }`. The initial state's `on` map must have a key that **exactly matches the signal name** — or the event is silently ignored and no session starts. ```yaml theme={null} # Signal name is "review-pr" fsm: initial: idle states: idle: on: review-pr: # must match signal name exactly target: clone ``` ## Action types Each entry array in a state takes one or more actions: **Agent action** — invoke an agent and optionally capture its output: ```yaml theme={null} entry: - type: agent agentId: my-agent # must match an agent declared in agents: prompt: "Do the thing" # optional per-step prompt outputTo: my-result # save output as a named document inputFrom: prev-result # feed a prior step's output as input ``` **Emit action** — advance the FSM after the agent finishes: ```yaml theme={null} entry: - type: agent agentId: my-agent outputTo: result - type: emit event: DONE ``` Agents do not auto-advance the FSM. Every action state needs an explicit `type: emit` to trigger the transition. The event name must exactly match a key in the `on` map. ## Passing data between steps Use `outputTo` and `inputFrom` to chain steps: ```yaml theme={null} step-a: entry: - type: agent agentId: agent-a outputTo: step-a-result # saves output as named document - type: emit event: DONE on: DONE: target: step-b step-b: entry: - type: agent agentId: agent-b inputFrom: step-a-result # receives step-a's output as task input - type: emit event: DONE ``` A step can take multiple prior outputs by passing `inputFrom` as an array — the engine concatenates them: ```yaml theme={null} summarize: entry: - type: agent agentId: summarizer inputFrom: [emails-result, calendar-result] ``` ## Running jobs * **Signals** — external events like webhooks or cron schedules * **Studio UI** — click **Run** on any job card in the [Studio](/guides/friday-studio) * **API** — `POST /api/workspaces/:id/signals/:signalId` * **CLI** — `friday signal trigger -n -w ` ## Memory Jobs can read and write space memory through agents. Memory persists across sessions — agents can recall what happened in previous runs. See [Memory](/core-concepts/memory). ## Inspecting jobs Use the [Job Inspector](/guides/job-inspector) to visualize the FSM as a DAG, run jobs with custom inputs, and debug executions with the waterfall timeline. ## Common gotchas * **`type: atlas` agents ignore the `tools` array.** Built-in agents are self-contained. If you need MCP tools called, use `type: llm` with an explicit `tools` array instead. * **Missing `type: emit`** — if a state has no emit action, the FSM never transitions and the session hangs. * **Emit name mismatch** — `event: DONE` in the emit must match the `on: DONE` key exactly. * **`outputTo` missing between chained steps** — the next step's `inputFrom` receives nothing. Always pair them. # MCP Tools Source: https://docs.hellofriday.ai/core-concepts/mcp Connect agents to external systems via Model Context Protocol servers. MCP is an open standard for connecting AI agents to external systems — databases, APIs, filesystems, and services. Friday manages MCP server processes: declare a server in your space and every `llm` or `user` agent in that space can call its tools. ## Declaring a server ```yaml workspace.yml theme={null} tools: mcp: servers: github: transport: type: stdio command: npx args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_PERSONAL_ACCESS_TOKEN: from_environment time: transport: type: stdio command: uvx args: ["mcp-server-time", "--local-timezone", "UTC"] ``` Each key under `servers` is the server ID you'll use to prefix tool names. The `transport` block tells Friday how to start the server. Use `from_environment` for credentials — the daemon injects them at process start. ## Tool naming Tools must be referenced as `serverId/toolName` in an agent's `tools` array: ```yaml theme={null} # Server ID is "github", tool name is "create_pull_request_review" tools: - github/create_pull_request_review - github/get_pull_request ``` Run `friday agent list -w ` to see the exact tool names available from each configured server. **Built-in platform tools** (`memory_save`, `memory_read`) have no prefix and require no MCP server declaration — they're always available: ```yaml theme={null} tools: - memory_save - memory_read - github/search_issues ``` ## Using tools in an llm agent ```yaml workspace.yml theme={null} agents: pr-reviewer: type: llm description: "Reviews pull requests using GitHub tools" config: provider: anthropic model: claude-sonnet-4-6 prompt: | Review the pull request and post a summary comment. tools: - github/create_pull_request_review - github/get_pull_request - github/get_pull_request_files ``` The LLM decides when and how to call tools based on the task and prompt. ## Using tools in a custom Python agent Python agents call tools via `ctx.tools` — tool names are bare (no prefix) when calling from code: ```python theme={null} result = ctx.tools.call("get_pull_request", {"owner": "myorg", "repo": "myrepo", "pull_number": 42}) ``` See the [MCP tools guide](/sdk/guides/use-mcp-tools) for the full Python API. ## atlas agents and MCP `type: atlas` built-in agents are self-contained — they have their own built-in tool surfaces and **ignore the `tools` array entirely**. If you need a specific MCP tool called, use `type: llm` instead. ## Common servers | Server | Install | What it provides | | ---------- | ------------------------------------------------ | --------------------------------- | | GitHub | `npx -y @modelcontextprotocol/server-github` | Repos, PRs, issues, commits | | Postgres | `npx -y @modelcontextprotocol/server-postgres` | SQL queries on Postgres | | Filesystem | `npx -y @modelcontextprotocol/server-filesystem` | Read/write local files | | Time | `uvx mcp-server-time` | Current time, timezone conversion | | Fetch | `uvx mcp-server-fetch` | HTTP fetch as a tool | Browse the full registry at [github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers). ## Transport `stdio` and `streamable HTTP` transports are supported. SSE transport is planned for a future release. # Memory Source: https://docs.hellofriday.ai/core-concepts/memory Persistent state that accumulates across sessions and auto-injects into agent context. Memory lets a space remember what happened across sessions. Friday automatically injects the 20 most recent entries from each narrative store into agent context at the start of every session — agents don't need to explicitly fetch memory for it to be available. ## How memory works Each space declares one or more memory stores. At session start, the platform injects recent entries into the agent's system prompt as labeled blocks: ```xml theme={null} - PR review for myorg/myrepo #42 completed — 2 critical findings (2026-04-29) - User prefers compact summaries ``` Agents can also write to memory explicitly during a job, recording summaries and facts for future sessions. ## Declaring memory stores ```yaml workspace.yml theme={null} memory: own: - name: notes type: short_term strategy: narrative - name: history type: long_term strategy: narrative ``` Memory `own` is a list of store objects, each with a `name`, `type`, and `strategy`. | Field | Options | Description | | ---------- | ------------------------- | --------------------------------------------------------------------- | | `type` | `short_term`, `long_term` | Short-term suits rolling context; long-term accumulates durable facts | | `strategy` | `narrative` | Narrative stores auto-inject into agent context | ## What auto-injects The 20 most recent entries from every narrative store in the space are injected at session start. You don't configure this per-agent — if the space has memory, every agent gets it. ## Reading and writing memory Agents use built-in platform tools — no MCP server required: * `memory_save` — write a new entry to a store * `memory_read` — explicitly fetch entries (for time-filtering or reading beyond the 20-entry window) * `memory_remove` — remove a stale entry by ID In a job, an LLM agent can write a session summary to memory: ```yaml workspace.yml theme={null} agents: session-summarizer: type: llm description: "Summarizes the session and writes to memory." config: provider: anthropic model: claude-haiku-4-5 prompt: | Summarize what happened in this session in 1-2 sentences. Write the result to memory using memory_save. tools: - memory_save ``` ## Keep entries terse Memory is injected on every session — verbose entries waste context tokens and dilute signal. Rules: * One fact per entry, under \~100 characters * No preamble ("The user said that…") — write the fact directly * Suffix time-sensitive entries with `(YYYY-MM-DD)` **Good:** `PR review for myorg/repo #42 — 2 critical findings (2026-04-29)` **Avoid:** `The user asked me to review a pull request and I found that there were two critical security issues in the authentication module` ## Large content: use artifact references Don't write large results directly to memory - they'll bloat every future session's context. Instead save the content as an artifact and store a short reference: **Step 1 - save the artifact**\ The agent creates an artifact via an LLM tool call. This returns `art_abc123`. **Step 2 - write the reference to memory**\ The agent calls the `memory_save` tool with a terse string: ``` memory_save("Q1 analysis report -> art_abc123 (2026-04-29)") ``` **Step 3 - retrieve later**\ In a future session, the agent reads memory (auto-injected or via `memory_read`), sees the artifact ID, and fetches the full content: ``` artifacts_get(id="art_abc123") ``` ## Memory mounts Spaces can mount memory from other spaces — useful when multiple spaces share context: ```yaml workspace.yml theme={null} memory: own: - name: notes type: short_term strategy: narrative mounts: - name: shared-kb source: "other-workspace-id/own/notes" mode: ro scope: space ``` `mode` is `ro` (read-only) or `rw` (read-write). Mounted stores also auto-inject into agent context. # Signals Source: https://docs.hellofriday.ai/core-concepts/signals Triggers that start jobs — HTTP webhooks, cron schedules, and filesystem events. Signals are how external events reach your Friday instance and trigger jobs. Every job runs in response to a signal. Signals live in `workspace.yml` under the `signals:` key. Each entry has a `provider` (which determines how the event arrives) and a `config` block with provider-specific settings: ```yaml workspace.yml theme={null} signals: daily-digest: title: "Daily digest" description: "Runs every morning at 9 AM local time" provider: schedule config: schedule: "0 9 * * *" timezone: "America/Los_Angeles" ``` ## Available signals * **[HTTP](/reference/signals/http)** — webhooks from services like GitHub, Bitbucket, or Jira; also accepts direct API calls for manual triggers * **[Schedule](/reference/signals/schedule)** — cron-based timers * **[Filesystem](/reference/signals/fs-watch)** — fires on file or directory changes Looking for chat integrations? See **[Communicators](/core-concepts/communicators)** for connecting Slack, Discord, Telegram, WhatsApp, or Microsoft Teams. ## Triggering signals manually HTTP signals can be triggered programmatically — the same endpoint that receives webhooks also accepts direct calls: ```bash theme={null} curl -X POST http://localhost:18080/api/workspaces//signals/ \ -H 'Content-Type: application/json' \ -d '{ "payload": { "foo": "bar" } }' ``` In Friday, manual triggering is just a POST to the HTTP signal's API route — there's no separate "manual" provider to configure. CLI and Studio are wrappers around this call: * **Studio** — *Run* button on any signal in the space dashboard * **CLI** — `friday signal trigger -n -w ` * **API** — `POST /api/workspaces/{workspaceId}/signals/{signalId}` (see the [Signals API](/api-reference/introduction)) ## Managing signals * **Studio** — view every signal in the [space dashboard](/guides/friday-studio), inspect its config, and see recent executions * **CLI** — `friday signal list` / `friday signal trigger` * **API** — the Signals endpoint accepts a `payload` and optional `streamId` for real-time progress # Skills Source: https://docs.hellofriday.ai/core-concepts/skills Reusable instruction sets loaded into agent context on demand. Package your standards, domain knowledge, and workflows into versioned Markdown files. Skills are instruction sets loaded into an agent's context window on demand. Package your coding standards, review criteria, domain knowledge, or any repeatable workflow into a `SKILL.md` file — agents load the right one for each task and follow it exactly. Skills are available globally across your Friday instance by default. You can also assign them to a specific space or job by referencing them in `workspace.yml`. ## Adding skills Open **Skills** in the Studio sidebar and click **+ Add**. Two ways to add: **Upload file / folder** — drop a folder that contains a `SKILL.md` file, or click to browse. Friday reads the frontmatter and registers the skill. **Import from skills.sh** — paste an `owner/repo/slug` reference (e.g. `anthropics/skills/pdf`) and click **Import skill**. Browse the full registry at [skills.sh](https://skills.sh). ## Writing a skill A skill is a folder with a `SKILL.md` file. The file starts with YAML frontmatter: ```markdown theme={null} --- name: my-namespace/review-criteria description: Applies code review standards for the payments team. Use when reviewing PRs touching the billing or payments modules. --- Your instructions here... ``` Key rules: * `name` must be `@namespace/skill-name` format — lowercase, hyphens only * `description` drives skill discovery — write it in third person and include a clear trigger ("Use when...") * Keep the body focused. The agent is already smart; only include what it would otherwise get wrong. ## Publishing via CLI ```bash theme={null} friday skill publish -p ./my-skill-folder ``` The directory must contain a `SKILL.md`. The skill name is read from the frontmatter `name` field, or you can override it with `--name @namespace/skill-name`. Other CLI commands: ```bash theme={null} friday skill list # list all published skills friday skill get @namespace/skill-name # get skill details friday skill versions @namespace/skill-name # list all versions ``` ## Using skills in workspace.yml Reference a published skill from your space: ```yaml workspace.yml theme={null} skills: - name: "@my-namespace/review-criteria" - name: "@my-namespace/sql-standards" version: 3 # pin to a specific version; omit for latest ``` Or define a skill inline without publishing it: ```yaml workspace.yml theme={null} skills: - name: quick-note inline: true description: Brief formatting rules for this space. instructions: | Always respond in bullet points. Keep each point under 15 words. ``` ## How agents load skills At the start of a session, Friday injects an `` list into the agent's context — each entry shows the skill name and its `description`. The agent calls the `load_skill` tool by name when it determines a skill is relevant to the current task. This is why the `description` field matters more than anything else in the file. It's the only thing the agent sees before deciding whether to load a skill. Write it in third person with a clear trigger: what the skill covers and when to use it. # Spaces Source: https://docs.hellofriday.ai/core-concepts/spaces Top-level containers for your agents, jobs, and signals — driven by workspace.yml. Spaces are the top-level container in Friday. Each space represents a complete configuration of agents, jobs, and signals — all defined in a single `workspace.yml` file. ## What is in a space A space contains: * **Agents** — the built-in, custom Python, or inline LLM agents available for jobs to use * **Jobs** — the FSM workflows that orchestrate agents step by step * **Signals** — the triggers that start jobs (HTTP webhooks and manual API calls, cron schedules, filesystem events, and chat platforms like Slack, Discord, Telegram, and WhatsApp) * **Skills** — structured instruction sets attached to agents for consistent output * **Memory** — persistent state across sessions, automatically injected into agent context * **MCP tools** — external tool servers (GitHub, Postgres, Notion, etc.) agents can call * **Configuration** — environment variables, credentials, and settings ## workspace.yml Everything in a space is driven by a `workspace.yml` file. This makes your configuration versionable, shareable, and repeatable. You can check it into source control, share it with teammates, or use it as a template for new spaces. ## Managing spaces * **Studio** — drag and drop a `workspace.yml` to add a space, or use the [space dashboard](/guides/friday-studio) to view and manage existing spaces * **CLI** — use `friday workspace list`, `friday workspace add`, and `friday workspace remove` * **API** — use the [Spaces API endpoints](/api-reference/introduction) to manage spaces programmatically * **Bundles** — pack a single space (or your whole instance) as a portable zip and re-import it on another machine. Bundles carry runtime data and, optionally, narrative memory — see [Backup and restore](/guides/friday-studio#backup-and-restore). ## Starter spaces Friday ships with starter spaces you can browse and import from **Discover Spaces** in the Studio. Each one is a working example you can customize. # Communicators Source: https://docs.hellofriday.ai/getting-started/communicators Connect a Communictor and chat with Friday on the go Communicators connect a Friday space to a chat platform so users can converse with it on the platform of their choice. The same agents, skills, and state that power Friday’s web chat respond on Slack, Discord, Telegram, WhatsApp, or Microsoft Teams — you just plug in a bot. ## Setup **In Friday Studio**, navigate to your space in the left sidebar and click **Overview**, then click the **Info** tab. Next, scroll down to the **Communicators** section and click **Connect** next to the platform you want. Paste the required credentials, and submit. Studio stores them securely and wires up the webhook for you. Communicators card in Studio with Connect buttons for slack, telegram, discord, teams, and whatsapp ### Platform Credentials **In the supported platform**, create the bot and copy the credentials (bot token, signing secret, etc.). Each provider page walks through this: * **[Slack](/guides/communicators/slack)** * **[Discord](/guides/communicators/discord)** * **[Telegram](/guides/communicators/telegram)** * **[WhatsApp](/guides/communicators/whatsapp)** * **[Microsoft Teams](/guides/communicators/teams)** ## Configuration Once connected, the space's `workspace.yml` records which communicators are active: ```yaml workspace.yml theme={null} communicators: slack: kind: slack ``` No secrets are stored in the YAML — credentials are stored separately and scoped to the space. ## What’s next Connect a Slack app to a space for DMs and @mentions. Connect a Discord bot to a space for DMs and @mentions. Connect a Telegram bot to a space for direct messages. Connect a WhatsApp Business number to a space for customer chat. Connect a Microsoft Teams bot to a space for DMs and @mentions. # Quick Start Source: https://docs.hellofriday.ai/getting-started/quickstart Install Friday Studio and run your first AI workflow in under 5 minutes. Friday Studio runs AI workflows on your Mac. The fastest way to see it work is to import a ready-made space and trigger it. This guide takes you from zero to a running workflow in under 5 minutes. **You'll need:** * A Mac (macOS 12 or later) * An [Anthropic API key](https://console.anthropic.com) Friday is primarily tested and recommended with Anthropic/Claude. OpenAI, Gemini, and Groq models are also supported as beta. ## Install Friday Studio Download and install Friday Studio from [hellofriday.ai](https://hellofriday.ai). Run the `.dmg`, drag Friday Studio to your Applications folder, and launch it. Read through, scroll to the bottom, and hit **Accept and Continue**. Paste your Anthropic API key when prompted. This is the recommended starting point. OpenAI, Gemini, and Groq are also supported in beta and can be configured at the same time. Friday downloads and configures its services, which takes about 3 minutes. The Studio opens in your browser at [http://localhost:15200](http://localhost:15200) when it's ready. In the left sidebar, click **Discover Spaces**. Import any starter space. [Inbox Zero](https://github.com/friday-platform/friday-studio-examples/tree/main/inbox-zero) or [GitHub Digest](https://github.com/friday-platform/friday-studio-examples/tree/main/github-digest) are the fastest to get running. Once imported, open the space, connect the required tools, and hit **Run**, or send a message in the chat panel to trigger it. ## Next: Spaces Spaces are the core building block in Friday. Each one is a self-contained workspace with its own agents, jobs, signals, and memory. [Learn how Spaces work →](/getting-started/spaces) ## Explore further Every built-in agent (Gmail, GitHub, Slack, Calendar, Claude Code, and more) and how to use them. How to wire agents together into multi-step pipelines with data passing between steps. Schedules, webhooks, Slack triggers, and how to control when your workflows run. A dozen ready-made spaces covering email, GitHub, Telegram, Google Sheets, price monitoring, and more. Trigger jobs, stream logs, and manage spaces without leaving your terminal. # Spaces Source: https://docs.hellofriday.ai/getting-started/spaces Create or import Spaces into Friday Studio and run your first AI workflow in under 5 minutes. Spaces are where you get work done in Friday — each one is a collection of agents, jobs, and signals focused on a specific task. ## Create your own Go to **Chat** and describe what you want: > "I want a space that monitors my GitHub notifications every morning and sends me a digest of open PRs and reviews waiting on me." Friday creates the space, wires up the agents and schedule, and registers it all through conversation. You can keep refining it by chatting. As you define your space in chat, Friday will ask clarifying questions and request login details where appropriate. Friday authorizes with external services via OAuth or API Keys. Authorization can be revoked at any time. ## Import an existing Space Friday comes with starter spaces you can import from the library. Using the **Inbox Zero**, which triages your Gmail and learns your preferences over time: In the Studio sidebar, click **Discover Spaces**. You'll see the full space library. Find **Inbox Zero** and click **Add Space**. Friday imports it and takes you to its dashboard. The space README tells you what to set up. For Inbox Zero: click **Connect** on the Gmail integration. Sign in with Google and approve access. Each space has a short README with any setup specific to that workflow — usually just connecting an account or filling in your email address. Read it, follow the steps. ### Upload your own You can upload a Space's .zip bundle by clicking the "Plus" icon next to "Spaces" in the Friday Studio sidebar, or by clicking **Settings** in the sidebar and navigating to "Import a space" under "Backup & restore" ## Run your Space Spaces use Signals to trigger Agents and Jobs. If you requested your Space to run on a regular schedule, the signal will run on a regular interval via CRON. You can also manually run a Signal at any time via chat. Let's use the Inbox Zero example from the "Import an existing Space" above: Go to the **Chat** tab in your Inbox Zero space and say: > "Review my inbox" Friday fetches your 10 most recent unread emails and walks you through them one at a time. For each one, you choose what to do — archive, keep, delete, or unsubscribe. Friday acts on your choice immediately. After the first session, it starts remembering your patterns. Run it again and it surfaces suggested actions based on what you've done before. Enable the autopilot job and it runs every morning at 8am, handles the high-confidence calls itself, and flags anything it's unsure about. Every Space in the library works this way — import, connect, run. The README tells you exactly what each one needs. ## Review configuration Every Space can be viewed as a single `workspace.yml` file where you can read, edit, and update its version. Whether created via chat or imported, the underlying config looks like this: ```yaml workspace.yml theme={null} version: "1.0" workspace: name: My Workflow description: What this space does signals: run-now: provider: http title: Run now description: Trigger manually config: path: /run-now agents: my-agent: type: atlas agent: web description: Does the thing prompt: | Describe exactly what you want the agent to do. jobs: my-job: title: My Job description: What this job does triggers: - signal: run-now fsm: initial: idle states: idle: on: run-now: target: run run: entry: - type: agent agentId: my-agent - type: emit event: DONE on: DONE: target: done done: type: final ``` Space configs are accessible by going to **Overview** under you Space in the Friday Studio sidebar and clicking "Edit Configuration" in the top right corner of the screen ## What’s next 9 ready-made spaces covering email, GitHub, Telegram, Google Sheets, price monitoring, and more. Connect a Communictor and chat with your Space on the go. # Testing Agents Source: https://docs.hellofriday.ai/guides/agent-tester Test any agent in isolation with different inputs, models, and configurations before wiring into workflows. The Agent Tester lets you run any [built-in agent](/core-concepts/agents) in isolation — test different inputs, models, and configurations before wiring agents into full workflows. Open it from the **Tools** section in the sidebar. ## Built-in agents Browse all available agents in a searchable catalog. Use **Cmd+K** (**Ctrl+K** on Windows/Linux) to focus search, arrow keys to navigate, and **Enter** to expand an agent's spec sheet. Press **Cmd+Enter** (**Ctrl+Enter**) to open the workbench. When you select an agent, the workbench opens with: * **Prompt input** — type your prompt and press **Cmd+Enter** (**Ctrl+Enter**) to execute. Use up/down arrows to cycle through prompt history. * **Example prompts** — expandable examples from the agent's documentation to help you get started. * **Credential panel** — shows required and optional credentials with connection status. Connect OAuth providers, enter API keys, or manually override any credential. Required credentials block execution until connected. * **Environment variables** — key-value editor for any additional configuration the agent needs. * **Artifact upload** — drag and drop files for agents that work with artifacts (CSVs, audio files, databases). * **Reference panel** — input/output JSON schemas showing what the agent expects and produces. After executing, the output streams in real time showing text, tool calls (with expandable input/output), and final results. Stats show tokens used, step count, and duration. Each execution is saved in your run history. ## Custom agents Build and test ad-hoc agents without editing `workspace.yml`. Configure: * **Provider** — Anthropic, OpenAI, Google, or Groq * **Model** — dropdown of available models (defaults to Claude Sonnet) * **System prompt** — custom instructions for the agent * **MCP servers** — multi-select to attach tool servers * **Environment variables** — additional configuration Type a prompt, execute, and see streaming results. This is useful for prototyping agent configurations before adding them to your space. ## Tips Different models yield different results. Sometimes an intelligent model like Opus yields the worst output for your use case — and a faster, cheaper model like Haiku or Sonnet works better. Use the Agent Tester to experiment before committing to a configuration. # Discord Source: https://docs.hellofriday.ai/guides/communicators/discord Connect a Discord bot to a space for DMs and @mentions. Connect a Discord bot to any Friday space so users can chat with it over DM or `@mentions`. Messages flow into the same conversation pipeline the web chat uses, and replies go back to Discord. Unlike the other communicators, Discord doesn't need a public URL — your Friday instance connects out to Discord and holds the connection open. ## Prerequisites * A Discord account that can create applications. * A Discord server where you can invite the bot — if you don't have one, create a personal test server for free. * Friday running. You don't need a public URL or tunnel for Discord. ## Setup 1. Open [discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application**. 2. Pick a name (e.g. *Friday Atlas*) and accept the developer ToS. 3. On the **General Information** page, copy two values you'll need later: * **Application ID** — a long numeric string * **Public Key** — a 64-character hex string 1. In the left sidebar, click **Bot**. 2. Click **Reset Token** (or **Add Bot** if this is the first time). Discord shows the token **once** — copy it immediately. Still on the **Bot** page, scroll to **Privileged Gateway Intents** and toggle **Message Content Intent** to **on**. Save changes. Without this toggle, every incoming message arrives with empty text — nothing will trigger the bot. 1. In Studio, select your space from the sidebar and click **Info**. 2. Find the **Communicators** card and click **Connect** next to **Discord**. 3. Paste the **Bot Token**, **Application ID**, and **Public Key** into the form, then submit. Studio stores the credentials securely and connects to Discord. The status flips to **Connected** when the bot comes online. Discord bots can only DM users once both sides share at least one server, and `@mentions` require a channel to mention in. 1. In the Developer Portal, go to **OAuth2** → **URL Generator**. 2. Under **Scopes**, check **`bot`** (do **not** check `applications.commands` — Friday doesn't use slash commands). 3. Under **Bot Permissions**, check: * **Read Messages/View Channels** * **Send Messages** * **Read Message History** 4. Copy the **Generated URL** at the bottom of the page. 5. Open the URL in a browser, pick the server, and click **Authorize**. 1. In Discord, DM the bot (click its name in your server's member list → **Message**). Send "hello friday". 2. Within a second or two, a new chat appears in Studio with a **DISCORD** badge. 3. To `@mention` the bot in a channel, make sure the bot can read the channel and post `@ ping`. Bot replies to `@mentions` appear in a Discord thread off the original message, not inline in the channel. ## Troubleshooting The most common cause is **Message Content Intent disabled**. Toggle it on in the Developer Portal → **Bot** page and reconnect via Studio. The bot token was rejected (revoked, expired, or mistyped). Regenerate the token in the Developer Portal and reconnect via Studio. Usually a network block: outbound WebSocket to Discord is blocked by a corporate firewall, VPN, or egress policy. Test from the same host with `curl -v https://discord.com/api/v10/gateway`. ## Known limitations * **Messaging only — no slash commands or button handlers.** Only DMs and `@mentions` are handled. * **Message Content Intent is privileged.** For bots in **100+ servers**, Discord additionally requires account verification and an intent review. ## Configure via YAML For CI or fully scripted setups, paste credentials directly into `workspace.yml` or the `.env` file in the Friday home directory (default `~/.friday/local/.env`). All three of `bot_token` / `public_key` / `application_id` must resolve. ```yaml workspace.yml theme={null} communicators: discord: kind: discord bot_token: MTIzNDU2Nzg5... public_key: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 application_id: "1234567890123456789" ``` ```bash # Friday home .env (default ~/.friday/local/.env) theme={null} DISCORD_BOT_TOKEN=MTIzNDU2Nzg5... DISCORD_PUBLIC_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 DISCORD_APPLICATION_ID=1234567890123456789 ``` ```yaml workspace.yml theme={null} communicators: discord: kind: discord ``` Restart Friday so the new config is picked up. # Slack Source: https://docs.hellofriday.ai/guides/communicators/slack Connect a Slack app to a space for DMs and @mentions. Connect a Slack app to any Friday space so users can chat with it over DM or `@mentions`. Messages flow into the same conversation pipeline the web chat uses, and replies go back to Slack. ## Prerequisites * A Slack space where you can install apps (admin, or one that allows member-installed apps). * Friday running with the bundled tunnel active (Slack requires a public HTTPS URL). ## Setup Open Studio → **Settings** → **Webhook tunnel** and copy the URL — something like `https://.trycloudflare.com`. Keep it handy for the next step. 1. Open [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** → **From a manifest**. 2. Pick the Slack space you want to install into. 3. Paste the manifest below, replacing `` with the tunnel host from the previous step: ```json theme={null} { "display_information": { "name": "friday-bot" }, "features": { "app_home": { "messages_tab_enabled": true, "messages_tab_read_only_enabled": false }, "bot_user": { "display_name": "friday-bot", "always_online": true } }, "oauth_config": { "scopes": { "bot": [ "app_mentions:read", "chat:write", "chat:write.public", "channels:history", "channels:read", "groups:history", "groups:read", "im:history", "im:read", "im:write", "mpim:history", "mpim:read", "mpim:write", "reactions:write", "users:read" ] } }, "settings": { "event_subscriptions": { "request_url": "https://.trycloudflare.com/platform/slack", "bot_events": ["message.im", "app_mention"] }, "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false } } ``` 4. Click **Next** → **Create**. Slack provisions scopes, event subscriptions, and the App Home messages tab from the manifest. 1. Go to **OAuth & Permissions** and click **Install to Space**. Approve the OAuth prompt. 2. From the app dashboard, copy: * **App ID** — under *Basic Information* → *App Credentials* * **Signing Secret** — same section, click **Show** to reveal * **Bot User OAuth Token** — starts with `xoxb-…`, under *OAuth & Permissions* Use the *Bot* OAuth Token (`xoxb-...`), not the *User* OAuth Token. They look similar but serve different purposes. 1. In Studio, select your space from the sidebar and click **Info**. 2. Find the **Communicators** card and click **Connect** next to **Slack**. 3. Paste the **App ID**, **Signing Secret**, and **Bot Token** into the form, then submit. Studio stores the credentials securely and wires the webhook for you. The status flips to **Connected** when it's done. Back in Slack's **Event Subscriptions**, the Request URL should report **Verified** once Friday is running. If it shows a yellow banner, click **Retry**. 1. In Slack, find the app under **Apps** in the sidebar, or DM it by name. 2. Send a message. The first message creates a chat in the space — it should appear in Studio with a blue **SLACK** badge, and replies flow back to Slack automatically. 3. To talk to the bot in a channel, invite it first (`/invite @bot-name`), then `@bot-name hello`. **You must `@`-mention the bot every time you message it in a channel.** A plain message in a channel where the bot is present will not reach Friday. DMs to the bot don't need the mention. ## Troubleshooting Check that the tunnel is running — Studio → **Settings** → **Webhook tunnel** should show **active** with a URL. If the tunnel just restarted, Cloudflare assigns a new URL — paste the fresh one into the manifest's `request_url`. Double-check that you connected Slack in Studio for **this** space, and that the App ID you pasted matches the one in Slack's *Basic Information*. The Signing Secret you pasted doesn't match the current one. In the Slack app dashboard click **Show** under *Signing Secret*, copy fresh, and reconnect via Studio. Invite the bot to the channel first (`/invite @bot-name`). ## Configure via YAML For CI or fully scripted setups, paste credentials directly into `workspace.yml` or the `.env` file in the Friday home directory (default `~/.friday/local/.env`). `app_id` is always required. ```yaml workspace.yml theme={null} communicators: slack: kind: slack app_id: A01234567 bot_token: xoxb-... signing_secret: ``` ```bash # Friday home .env (default ~/.friday/local/.env) theme={null} SLACK_BOT_TOKEN=xoxb-... SLACK_SIGNING_SECRET= ``` ```yaml workspace.yml theme={null} communicators: slack: kind: slack app_id: A01234567 ``` Save and restart Friday. You'll still need to set Slack's **Request URL** to `/platform/slack` and subscribe to `message.im` and `app_mention` events — the manifest above does this in one shot. # Microsoft Teams Source: https://docs.hellofriday.ai/guides/communicators/teams Connect a Microsoft Teams bot to a space for DMs and @mentions. Connect a Microsoft Teams bot to any Friday space so users can chat with it over DM or `@mentions`. Messages flow into the same conversation pipeline the web chat uses, and replies go back to Teams. You create the Azure Bot at [portal.azure.com](https://portal.azure.com), build a Teams app package, and connect via Studio. ## Prerequisites * An Azure account with an active subscription. The **F0** pricing tier for Azure Bot is free and sufficient for development. * Admin rights in a Microsoft 365 tenant that lets you sideload custom Teams apps. A personal Microsoft account ("Teams free") does **not** work — you need a work/school tenant or a Microsoft 365 Developer Program tenant. * Friday running with the bundled tunnel active (Azure Bot requires a public HTTPS URL — `http://` is rejected). ## Setup 1. Open [portal.azure.com](https://portal.azure.com) and click **Create a resource** → search for **Azure Bot**. 2. Fill in: * **Bot handle** — a unique identifier, e.g. `friday-studio-bot` * **Pricing tier** — **F0** (free) * **Type of App** — **Single Tenant** if you only plan to install into one M365 tenant; **Multi Tenant** for cross-tenant distribution. This choice is **immutable** — see [SingleTenant vs MultiTenant](#singletenant-vs-multitenant). * **Creation type** — **Create new Microsoft App ID** 3. Click **Review + create** → **Create**. Provisioning takes 30–60s. The **Type of App** dropdown is permanently greyed out after the bot exists. If you pick the wrong one, you have to delete and recreate the Azure Bot resource. From the Bot resource you just created: | Value | Where | | ------------------------- | ------------------------------------------------------------------------------------- | | **Microsoft App ID** | Bot resource → **Configuration** → *Microsoft App ID* | | **Client secret value** | **Manage Password** → *Certificates & secrets* → **New client secret** → copy *Value* | | **Directory (tenant) ID** | App Registration → **Overview** → *Directory (tenant) ID* | The tenant ID is only required for SingleTenant bots, but it's harmless to set either way. Azure only shows the client secret **value** once. Copy it immediately — if you miss it, delete the secret and create a new one. 1. In Studio, select your space from the sidebar and click **Info**. 2. Find the **Communicators** card and click **Connect** next to **Microsoft Teams**. 3. Paste the **App ID**, **Client Secret**, **Tenant ID**, and select the **App Type** (SingleTenant or MultiTenant) into the form, then submit. Studio stores the credentials securely. The status flips to **Connected** when it's done. 1. Open Studio → **Settings** → **Webhook tunnel** and copy the URL — something like `https://.trycloudflare.com`. 2. In the Azure portal, open your Bot resource → **Configuration** → **Messaging endpoint** and set it to `/platform/teams`, e.g. `https://.trycloudflare.com/platform/teams`. 3. Click **Apply**. 4. Under **Channels**, click **Microsoft Teams**, accept the ToS, and enable the channel. Azure Bot is the API endpoint; the Teams app package is what puts your bot in front of real users. It's a zip containing `manifest.json` plus two icons. 1. Create a directory with this `manifest.json` (replace `` with your Microsoft App ID): ```json manifest.json theme={null} { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", "manifestVersion": "1.16", "version": "1.0.0", "id": "", "packageName": "com.yourco.friday", "developer": { "name": "Your Team", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, "icons": { "color": "color.png", "outline": "outline.png" }, "name": { "short": "Friday", "full": "Friday Teams" }, "description": { "short": "Friday agent orchestration", "full": "Chat with your Friday space from Teams." }, "accentColor": "#4F46E5", "bots": [ { "botId": "", "scopes": ["personal"], "isNotificationOnly": false, "supportsFiles": false } ], "permissions": ["identity", "messageTeamMembers"], "validDomains": [] } ``` 2. Drop a **192×192 PNG** named `color.png` and a **32×32 PNG** named `outline.png` beside the manifest. 3. Zip those three files (manifest must be at the root of the archive, not inside a subfolder). 4. In Teams, go to **Apps** → **Manage your apps** → **Upload an app** → **Upload a custom app** and pick the zip. 5. Click **Add** when Teams prompts. The bot appears in your apps list. 1. Open the bot's app page in Teams and click **Chat** to start a DM. 2. Send a message. The first message creates a chat in the space — it should appear in Studio with a **TEAMS** badge, and replies flow back to Teams automatically. 3. To `@mention` the bot in a channel, add the app to the team first, then `@ hello`. ## SingleTenant vs MultiTenant | Type | Tenant ID required | When to pick | | ---------------- | ------------------ | --------------------------------------------------------------------- | | **SingleTenant** | Yes | You only install the bot into your own M365 tenant (most dev setups). | | **MultiTenant** | No | The same bot will be installed across multiple tenants (SaaS). | The **App Type** you select in Studio's connect form must match what the Azure Bot was created as. Mismatched values produce an `Authorization has been denied for this request` on outbound replies — inbound routing still looks fine, which makes it easy to miss. ## Troubleshooting `app_type` doesn't match the Bot Service's registered Type of App. Check Azure Bot → **Configuration** → *Type of App* and reconnect via Studio with the matching value. Most likely the **Teams channel isn't enabled** in Azure. Go to Bot resource → **Channels** → click **Microsoft Teams** → accept ToS and enable. Second most likely: the messaging endpoint in Azure points at a stale tunnel URL. The incoming JWT is being rejected. Usual causes: the App ID you pasted doesn't match the Bot's Microsoft App ID, or a missing tenant ID on a SingleTenant bot. The bundled Cloudflare quick-tunnel gets a new random URL every restart. Each restart requires updating the **Messaging endpoint** in the Azure portal. For stable development, use a named Cloudflare tunnel, ngrok paid, or a real reverse proxy on your own domain. ## Production notes * Rotate the client secret under *App Registration → Certificates & secrets*. Azure doesn't let you edit an existing secret — create a new one, reconnect via Studio with the new value, then delete the old one. * Client secrets expire (default 24 months). When they do, every outbound reply returns 401 until you rotate. ## Configure via YAML For CI or fully scripted setups, paste credentials directly into `workspace.yml` or the `.env` file in the Friday home directory (default `~/.friday/local/.env`). `app_id` is always required. ```yaml workspace.yml theme={null} communicators: teams: kind: teams app_id: 05ad3c58-7bfc-41d6-a249-011a6fe5337b app_password: app_tenant_id: app_type: SingleTenant # or MultiTenant — must match Azure Bot's registered Type of App ``` ```bash # Friday home .env (default ~/.friday/local/.env) theme={null} TEAMS_APP_ID=05ad3c58-7bfc-41d6-a249-011a6fe5337b TEAMS_APP_PASSWORD= TEAMS_APP_TENANT_ID= TEAMS_APP_TYPE=SingleTenant ``` ```yaml workspace.yml theme={null} communicators: teams: kind: teams ``` Restart Friday so the new config is picked up. # Telegram Source: https://docs.hellofriday.ai/guides/communicators/telegram Connect a Telegram bot to a space for direct messages. Connect a Telegram bot to any Friday space so users can chat with it over DM. Messages flow into the same conversation pipeline the web chat uses, and replies go back to Telegram. ## Prerequisites * A Telegram account on your phone (the one that will own the bot). * Friday running with the bundled tunnel active (Telegram requires a public HTTPS URL). ## Setup 1. Open the **Telegram** app on your phone and search for **BotFather** (official account, blue checkmark). Tap **Start**. 2. Send `/newbot`. 3. Choose a **display name** (what users see, e.g. *Friday Studio*). 4. Choose a **username** ending in `bot` (e.g. `friday_studio_xxxxx_bot`). 5. BotFather replies with a **bot token** like `123456789:ABC-DEF-GHIJKLM...`. Treat it like a password — anyone with it controls the bot. 1. In Studio, select your space from the sidebar and click **Info**. 2. Find the **Communicators** card and click **Connect** next to **Telegram**. 3. Paste the **Bot Token** into the form, then submit. Studio stores the token securely and registers the webhook with Telegram automatically. The status flips to **Connected** when it's done. In Telegram, search for your bot by username (`@friday_studio_xxxxx_bot`) and tap **Start** or send a message. The first message creates a chat in your space — it should appear in Studio with a green **TELEGRAM** badge, and replies flow back automatically. ## Troubleshooting Most likely the tunnel URL is stale (the development tunnel gets a new URL on every restart). Reconnect Telegram in Studio so it re-registers the webhook against the current tunnel. Double-check you copied the full token from BotFather (numeric ID before the colon, secret after). Regenerate via BotFather → `/token` if needed. ## Configure via YAML For CI or fully scripted setups, paste the bot token directly into `workspace.yml` or the `.env` file in the Friday home directory (default `~/.friday/local/.env`). ```yaml workspace.yml theme={null} communicators: telegram: kind: telegram bot_token: 123456789:ABC-DEF-GHIJKLM... ``` ```bash # Friday home .env (default ~/.friday/local/.env) theme={null} TELEGRAM_BOT_TOKEN=123456789:ABC-DEF-GHIJKLM... ``` ```yaml workspace.yml theme={null} communicators: telegram: kind: telegram ``` When you skip Studio, you also need to register the webhook with Telegram yourself. Grab the tunnel URL from Studio → **Settings** → **Webhook tunnel** and run: ```bash theme={null} TELEGRAM_BOT_TOKEN="123456789:ABC-..." TUNNEL_URL="https://.trycloudflare.com" SUFFIX="${TELEGRAM_BOT_TOKEN#*:}" curl -s -X POST \ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \ -d "url=${TUNNEL_URL}/platform/telegram/${SUFFIX}" ``` Restart Friday after editing config. # WhatsApp Source: https://docs.hellofriday.ai/guides/communicators/whatsapp Connect a WhatsApp Business number to a space for customer chat. Connect a WhatsApp Business number to any Friday space so customers can chat with it over WhatsApp. Friday talks to the [WhatsApp Business Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api), so you'll create an app at [developers.facebook.com](https://developers.facebook.com/apps). **This is NOT WhatsApp Web.** `web.whatsapp.com` is the consumer client — bots can't be built on top of it. You need the *Business Cloud API*, which is a separate Meta developer product. ## Prerequisites * A Meta developer account — [developers.facebook.com](https://developers.facebook.com) * A Facebook Business Manager (free to create) * A phone number you can receive SMS/calls on for OTP verification (for production). For dev, Meta gives you a **free test number** that can message up to 5 verified recipients. * Friday running with the bundled tunnel active, or a real HTTPS domain. ## Setup 1. Open [developers.facebook.com/apps](https://developers.facebook.com/apps) and click **Create app**. 2. **App details** — pick an app name. Don't include the words `WhatsApp`, `FB`, `Face`, `Book`, `Insta`, `Gram`, or `Rift` — Meta's trademark filter rejects them silently (the Next button just stays disabled). 3. **Use cases** — choose **Other** → **Business** → then tick **Connect with customers through WhatsApp**. 4. **Business portfolio** — select an existing Business portfolio or create a new one. Pick **Verify later** unless you already have the business paperwork ready. 5. **Requirements → Overview → Create app.** Meta re-prompts for your Facebook password before finalizing. 6. On the "Welcome to WhatsApp Business Platform" page, accept the **Facebook Terms for WhatsApp Business** and **Meta Hosting Terms** and click **Continue**. 7. Keep the default **Opt in to all current and future WhatsApp accounts**, then **Continue** → **Save**. The big green **Generate access token** button on **WhatsApp → API Setup** gives you a *temporary* token that stops working after 24 hours. Don't use it. The System User token below never expires. 1. Open [business.facebook.com/latest/settings/system\_users](https://business.facebook.com/latest/settings/system_users) and select the right business at the top-left. 2. **Users** → **System users** → click **Add**. 3. Fill in the form — name the system user (avoid hyphens), set role to **Admin**, then **Create system user**. 4. Click **Assign assets** → pick **WhatsApp accounts** → check your WhatsApp Business Account → toggle **Everything** on → **Assign assets** → **Done**. 5. **Accounts → Apps**, click your app, then **Assign people** → check the system user → toggle **Manage app** on (Full control) → **Assign**. 6. Back on **System users → \** click **Generate token**: * Select your Meta app → **Next** * Set expiration: **Never** → **Next** * Tick both `whatsapp_business_messaging` and `whatsapp_business_management` → **Generate token** * Copy the token immediately — the popup shows it once. | Name | Where in the dashboard | | --------------- | ---------------------------------------------------- | | Phone number ID | WhatsApp → API Setup → *Phone number ID* | | App Secret | App Settings → Basic → **Show** next to *App Secret* | For **App Secret**, use the copy icon rather than retyping — hex fonts make `6c` look like `c`, and a single wrong character breaks signature verification. 1. In Studio, select your space from the sidebar and click **Info**. 2. Find the **Communicators** card and click **Connect** next to **WhatsApp**. 3. Paste the **Access Token**, **App Secret**, and **Phone Number ID** into the form, then submit. Studio stores the credentials securely and registers the webhook with Meta for you. The status flips to **Connected** when it's done. Meta requires you to opt in to the events you want to receive. 1. In the Meta dashboard, left nav **Configuration** (under "Connect on WhatsApp" use case). 2. In the **Webhook fields** table, find `messages` and toggle its **Subscribe** switch. Optionally also subscribe to `message_status` / `message_echoes`. If you're using Meta's free test number, WhatsApp will only deliver outbound messages to **pre-verified** recipients. Inbound is unrestricted. 1. Left nav → **API Setup**. 2. Click the **To** combobox → **Manage phone number list**. 3. Type the 10-digit national number. Click **Next**. Meta sends a WhatsApp message to that number with a **5-digit verification code**. 4. Enter the code into the verification modal. Production numbers (once registered and approved) can receive from anyone. From a verified WhatsApp number on your phone, send a message to the business number. It should appear in Studio with a green **WHATSAPP** badge, and replies flow back automatically. ## Troubleshooting Usually a token issue. Make sure your access token has both `whatsapp_business_messaging` and `whatsapp_business_management` scopes, and that the system user has been assigned the WhatsApp Business Account. `app_secret` doesn't match the App Secret under **App Settings → Basic**. Re-copy and reconnect via Studio; any whitespace or truncation fails verification. You're using the 24-hour *temporary* access token from **WhatsApp → API Setup** and it expired. Create a permanent System User token following the steps above and reconnect. Production requires registering a real business number with OTP verification — free test numbers are capped at 5 approved recipients and cannot be promoted. ## Production notes * Meta rotates App Secrets on request under **App Settings → Basic → Reset**. After rotating, reconnect WhatsApp in Studio so the new secret is stored. * WhatsApp message history **cannot** be fetched via the Cloud API — only messages received while Friday is running are available as conversational context. * You can't edit or delete already-sent WhatsApp messages. Streaming replies are sent as a single message once the response is complete. ## Configure via YAML For CI or fully scripted setups, paste credentials directly into `workspace.yml` or the `.env` file in the Friday home directory (default `~/.friday/local/.env`). You'll also need to set the callback URL and verify token in Meta's dashboard manually. ```yaml workspace.yml theme={null} communicators: whatsapp: kind: whatsapp access_token: "EAA..." app_secret: "0ad6c116..." phone_number_id: "15551234567" verify_token: "" api_version: "v21.0" # optional; defaults to v21.0 ``` ```bash # Friday home .env (default ~/.friday/local/.env) theme={null} WHATSAPP_ACCESS_TOKEN=EAA... WHATSAPP_PHONE_NUMBER_ID=15551234567 WHATSAPP_APP_SECRET=0ad6c116... WHATSAPP_VERIFY_TOKEN= ``` ```yaml workspace.yml theme={null} communicators: whatsapp: kind: whatsapp ``` Generate a verify token with `openssl rand -hex 32`. Then in the Meta dashboard's **Configuration** panel, paste the **Callback URL** as `/platform/whatsapp` and the **Verify token** you chose, click **Verify and save**, and subscribe to the `messages` field. # Using Friday Studio Source: https://docs.hellofriday.ai/guides/friday-studio Navigate the Studio UI — spaces, runs, jobs, skills, backup, and programmatic access. The Studio is the primary visual interface for Friday Studio. Open it in your browser after launching the app. ## Access Open [http://localhost:15200](http://localhost:15200) in your browser. The sidebar shows a green health dot when connected to the Friday daemon. ## Spaces ### Space dashboard When you select a space from the sidebar, the dashboard shows two tabs: **Activity** and **Info**. **Activity** shows your chat history for the space and recent job runs. Each run shows its status, what triggered it, a summary of what happened, and how long it took. A green dot indicates a run still in progress. Space Activity tab showing chat history and recent
runs **Info** shows the space's jobs, signals, agents, and communicators. Each job has a **Run** button to trigger it manually. Signals show their type (WEBHOOK or SCHEDULE) and the trigger URL or cron schedule. Agents are listed with their type, provider, model, and description. Communicators show which chat platforms are connected. Space Info tab showing jobs, signals, agents, and
communicators Space Info tab — agents and communicators
panels Click **Edit Configuration** in the top right to open the YAML editor. The **···** menu lets you export the configuration, download the space as a bundle, or remove it — see [Backup and restore](#backup-and-restore). ### Add a space Click **Discover Spaces** in the sidebar to browse and import starter spaces from the library. To load your own, click **+** and drop in a `workspace.yml` file. ### Edit configuration Click **Edit Configuration** to open a full-page YAML editor with syntax highlighting. Press **Cmd+S** (**Ctrl+S** on Windows) or click **Save** to apply changes. ## Runs ### Run list Click **Runs** in the sidebar for any space to see all executions. Each card shows the job name, status, a summary of what happened, and duration. ### Run detail Click any run to open the full execution view — a vertical timeline of every agent that ran, with duration shown per step. Click any agent block to expand it and see the full input, output, and any tool calls it made. The right sidebar shows the run ID, status, start time, duration, and step count. Run detail showing agent execution timeline and run
metadata Runs stream in real time — you can watch each agent step execute as it happens. ## Jobs The **Info** tab shows all jobs in the space. Each job card displays its description and a **Run** button. Click **Run** to trigger it — a dialog appears with any input fields defined by the signal's schema. Jobs list with Run button and trigger dialog The **···** menu on each job gives you: * **Copy as cURL** — a ready-to-paste curl command to trigger the job * **Copy CLI command** — the equivalent `friday signal trigger` command * **Edit configuration** — jumps to the YAML editor at the relevant section ## Skills browser Browse and manage published skills from **Skills** in the sidebar. The left panel shows skills organized by namespace. Click any skill to view its full content on the right. Drag and drop a `SKILL.md` or skill folder onto the upload zone to publish a new skill. Skills browser showing skill tree and skill detail ## Backup and restore The Studio packs spaces as portable zip bundles you can re-import on any machine running Friday. Imports are non-destructive — collisions are suffixed with a timestamp so you can merge manually. ### Export a single space Open any space and click the **···** menu in the top right: * **Export configuration** — just the `workspace.yml` * **Download space** — configuration plus run history and attached skills. Memory not included. * **Download space with notes & memory** — same, plus the space's narrative memory Space export menu ### Export your whole instance Open **Settings** → **Backup & restore** → **Download full export** to pack every space into a single archive. Toggles let you include notes & memory and the global skills library. Backup and restore in Settings ### Import The same **Backup & restore** section has two import options: * **Import a space** — a single-space zip * **Import a full archive** — a full export zip; every space inside is imported ## Programmatic access Everything in the Studio is powered by the [Friday daemon API](/api-reference/introduction). See the [API reference](/api-reference/introduction) and [CLI reference](/reference/cli) for programmatic access. The `friday` CLI ships at `~/.friday/local/bin/friday`. Make sure that directory is on your PATH — see the [CLI setup guide](/reference/cli#setup). # Call Friday from your app Source: https://docs.hellofriday.ai/guides/integrate-with-your-app Trigger a workflow, stream its progress, and cancel it — all from your own application. In this guide, you'll trigger a Friday workflow from your application, stream its progress in real time, and optionally cancel it. ## Prerequisites * Friday Studio [installed and running](/getting-started/quickstart) * At least one space loaded with a signal configured * The daemon API is available at `http://localhost:18080` Run `curl http://localhost:18080/api/workspaces` to see your loaded spaces and their IDs. Each space's `signals` object shows what signals are available and what payload fields they expect. ## Trigger a signal Every workflow starts with a **signal**. Send a POST request with a payload and Friday runs the job. ### Fire and wait Send a POST and block until the job completes: ```bash cURL theme={null} curl -X POST http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId} \ -H 'Content-Type: application/json' \ -d '{"payload": {"key": "value"}}' ``` ```typescript TypeScript theme={null} const response = await fetch( "http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId}", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payload: { key: "value" } }), } ); const result = await response.json(); console.log(result.sessionId); // use this to check status ``` ```python Python theme={null} import httpx response = httpx.post( "http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId}", json={"payload": {"key": "value"}}, ) response.raise_for_status() print(response.json()["sessionId"]) ``` **Response:** ```json theme={null} { "message": "Signal completed", "status": "completed", "workspaceId": "my-space", "signalId": "my-signal", "sessionId": "sess_abc123" } ``` ### Stream progress in real time Add `Accept: text/event-stream` to receive a live SSE stream as the job executes: ```bash cURL theme={null} curl -N -X POST http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId} \ -H 'Content-Type: application/json' \ -H 'Accept: text/event-stream' \ -d '{"payload": {"key": "value"}}' ``` ```typescript TypeScript theme={null} const response = await fetch( "http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId}", { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", }, body: JSON.stringify({ payload: { key: "value" } }), } ); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; for (const line of decoder.decode(value).split("\n")) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") break; console.log(JSON.parse(data)); } } } ``` ```python Python theme={null} import httpx with httpx.stream( "POST", "http://localhost:18080/api/workspaces/{workspaceId}/signals/{signalId}", json={"payload": {"key": "value"}}, headers={"Accept": "text/event-stream"}, ) as response: response.raise_for_status() for line in response.iter_lines(): if line.startswith("data: ") and line[6:] != "[DONE]": print(line[6:]) ``` **Event stream:** ``` data: {"type":"data-session-start","data":{"sessionId":"sess_abc123"}} data: {"type":"text-delta","delta":"Fetching emails..."} data: {"type":"job-complete","data":{"success":true,"sessionId":"sess_abc123","status":"completed"}} data: [DONE] ``` ## Check run status Use the `sessionId` from the trigger response to check on the run: ```bash cURL theme={null} curl http://localhost:18080/api/sessions/sess_abc123 ``` ```typescript TypeScript theme={null} const session = await fetch( "http://localhost:18080/api/sessions/sess_abc123" ).then(r => r.json()); console.log(session.status); // "completed", "running", or "failed" ``` ```python Python theme={null} import httpx session = httpx.get("http://localhost:18080/api/sessions/sess_abc123").json() print(session["status"]) ``` ## Cancel a running job If you need to stop a job mid-execution: ```bash cURL theme={null} curl -X DELETE http://localhost:18080/api/sessions/sess_abc123 ``` ```typescript TypeScript theme={null} await fetch("http://localhost:18080/api/sessions/sess_abc123", { method: "DELETE", }); ``` ```python Python theme={null} import httpx httpx.delete("http://localhost:18080/api/sessions/sess_abc123") ``` ## Next steps Full endpoint reference for every API operation. Signal types, payload schemas, and webhook configuration. # Inspecting Jobs Source: https://docs.hellofriday.ai/guides/job-inspector Debug pipeline executions with DAG (Directed Acyclic Graph) visualization and waterfall timelines. The Job Inspector is a debugging tool for pipeline executions. Open it from the **Tools** section in the sidebar. ## Pipeline diagram The top of the inspector shows a DAG of your job's pipeline steps. Select a space and job from the dropdown, then click any node to highlight it. ## Running a job Below the diagram, configure and launch a run: 1. Select a signal from the dropdown (if the job has multiple triggers). 2. Fill in the signal input form — fields are auto-generated from the signal's JSON schema. 3. Optionally disable specific steps by clicking the step chips (disabled steps are skipped during execution). 4. Click **Run** to start the pipeline. ## Waterfall timeline Once a run starts (or when you load a previous run), the waterfall timeline replaces the pipeline diagram: * Each agent step appears as a horizontal bar showing its duration relative to the run start * Color-coded status: green (completed), red (failed), yellow (running), gray (pending) * Click any row to open a detail panel with the step's full inputs, outputs, logs, and artifacts * The timeline auto-selects the first failed step when a run fails Total run duration displays at the top with tick marks for time intervals. ## Recent runs The inspector shows recent runs below the run controls. Click any run to load it into the waterfall timeline. Use the dropdown in the toolbar to jump between previous runs. # Sharing Spaces Source: https://docs.hellofriday.ai/guides/sharing-spaces Package a space for others — share the config, or hand off a full bundle with history and memory. A space bundle is a single `.zip` containing the full configuration, run history, attached skills, and optionally the space's narrative memory. Hand it to a teammate or carry it to another machine — the recipient imports the `.zip` and the space lands ready to run. ## Export a space Click any space in the Studio sidebar to open it. Click the **···** menu in the top right of the space. Pick one of: * **Download space** — configuration, run history, and attached skills. Memory not included. * **Download space with notes & memory** — same as above, plus the space's narrative memory. Friday writes a single `.zip` to your downloads folder. Hand it off however you like — email, shared drive, chat, USB. Space export menu Bundles include run history and (optionally) memory, which may contain details from your previous sessions. Review the contents before sharing outside your team. Credentials never travel with the bundle — API keys and OAuth tokens are stored separately by the Link service. The recipient connects their own accounts after import. See [Security](/security) for how Friday handles secrets. ## Import a space In the Studio sidebar, click **Settings**, then open the **Backup & restore** section. Click **Import a space** and select the `.zip` you received. Open the imported space and follow its README — usually a short list of services to sign in to (Google, Slack, etc.). Friday prompts for any integrations the space uses. Imports are non-destructive. If you already have a space with the same name, Friday adds a timestamp suffix so you can merge manually. ## What's next Full reference for export and import flows in the Studio. What's in a space and how `workspace.yml` is structured. # Friday Studio Source: https://docs.hellofriday.ai/index Import and build powerful AI workflows that run on your machine, on your schedule, with your data. Friday Studio is a desktop app that runs AI workflows on your machine. Describe what you want in the chat, and Friday builds it: the agents, the schedule, the connections between steps. The result is a `workspace.yml` file you can read, edit, share, and import on any machine. It runs exactly as built, every time. Every workflow is transparent configuration. Nothing is trapped in someone's head. Friday Studio currently supports macOS. Windows and Linux support are on the way. ## The building blocks Everything in Friday is built from three concepts. Once you understand them, any workflow is straightforward to reason about. How work starts. A signal is anything that tells Friday it's time to act, like a cron schedule, an incoming webhook, or a manual trigger from the UI. You define what to listen for; Friday handles the rest. What does the work. Friday ships with a full library of built-in agents covering web research, GitHub, Slack, Jira, Bitbucket, HubSpot, code execution, knowledge retrieval, and more. Each one is purpose-built for its domain. When you need something that doesn't exist, Friday writes the agent itself based on what you're trying to accomplish. How agents work together. A job is a pipeline that chains agents into a sequence, passing structured data from one step to the next. Steps can branch conditionally or wait on upstream results. The whole thing is defined in configuration, making it transparent, repeatable, and easy to modify. Put them together and you get workflows that trigger on their own, execute reliably across multiple agents, and produce consistent results every time. ## What people build with Friday Friday reviews your last 10 unread emails and walks you through them one at a time. Archive, keep, delete, or unsubscribe. It remembers what you chose and applies those patterns automatically next time. Set it to autopilot and it runs every morning at 8am, handles the high-confidence calls itself, and flags anything it's unsure about. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/inbox-zero) Every weekday at 7:30am, Friday pulls your calendar and emails, synthesizes them into a single briefing with your top priorities for the day, and delivers it to your inbox before you open your laptop. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/daily-operating-memo) Message a Telegram bot in plain English: "Had coffee with James today, follow up in two weeks." Friday remembers everything, surfaces who you should reach out to this week, and answers questions about your contacts. No forms, no spreadsheets. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/networking-crm) Paste a pull request URL in chat. Friday reads the full diff, analyzes for bugs, security issues, and style problems, then posts an inline review with line-level comments and a verdict directly to the PR. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/github-pr-reviewer) Every Monday morning, Friday scans for the past week of activity across product, pricing, partnerships, and leadership, and delivers a sourced briefing with confirmed dates and direct article links. No fabricated entries, no noise. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/competitive-monitor) Friday scrapes Best Buy, Newegg, Amazon, and B\&H every hour. When a listing drops under your target price and is in stock, it sends you an email with a direct buy link. Easily reconfigured for any product by asking Friday in chat. [See the space →](https://github.com/friday-platform/friday-studio-examples/tree/main/rtx-price-monitor) ## Frequently asked questions Yes, for most people. Friday is free if you're an individual using it for personal, non-commercial purposes, a team of fewer than 5 people, or a business with under \$1M in annual recurring revenue. For production use beyond those criteria, a commercial license is available from Tempest Labs. [Contact us](/contact#commercial-licensing) if that applies to you. Friday is source-available. You can read the code, fork it, and build on it under the terms of the [LICENSE](https://github.com/friday-platform/friday-studio/blob/main/LICENSE). Coming soon. [Let us know](/contact) if you have specific features you'd like to see in a cloud version; we're actively shaping it. Yes. We love working with teams building on Friday. [Reach out](/contact#implementation-support) to set up a time. ## What’s Next Install Friday Studio and run your first space in under 5 minutes. 9 ready-made spaces covering email, GitHub, Telegram, Google Sheets, price monitoring, and more. # CLI Source: https://docs.hellofriday.ai/reference/cli The friday command-line tool for managing spaces, agents, signals, and the Friday daemon. The `friday` CLI is how you interact with Friday from the terminal. It's a thin HTTP client over `localhost:18080` — every subcommand maps to one or more daemon routes. Use it to manage spaces, trigger signals, send prompts, publish skills, and control the daemon. ## Setup The installer puts the `friday` binary at `~/.friday/local/bin/friday`. Add that directory to your shell PATH. Add to `~/.zshrc`: ```bash theme={null} export PATH="$HOME/.friday/local/bin:$PATH" ``` Then reload: ```bash theme={null} source ~/.zshrc ``` Run once: ```bash theme={null} fish_add_path ~/.friday/local/bin ``` Or add to `~/.config/fish/config.fish`: ```bash theme={null} set -gx PATH $HOME/.friday/local/bin $PATH ``` Add to `~/.bashrc`: ```bash theme={null} export PATH="$HOME/.friday/local/bin:$PATH" ``` Then reload: ```bash theme={null} source ~/.bashrc ``` Add to your profile (`$PROFILE`): ```powershell theme={null} $env:PATH = "$env:USERPROFILE\.friday\local\bin;" + $env:PATH ``` Verify: ```bash theme={null} friday version ``` ## Spaces ### friday workspace list List all registered spaces. ```bash theme={null} friday workspace list friday workspace list --json ``` **Aliases:** `friday workspace ls`, `friday w list`, `friday work list` ### friday workspace add Register a space from a directory containing a `workspace.yml` file. ```bash theme={null} friday workspace add -p /path/to/workspace friday workspace add -p . --name my-space friday workspace add --scan /projects --depth 2 ``` | Flag | Description | | ------------------- | --------------------------------------- | | `-p` | Path to space directory (required) | | `--scan` | Scan a directory recursively for spaces | | `--depth` | Maximum scan depth (default: 3) | | `--name, -n` | Override space name | | `--description, -d` | Add a description | ### friday workspace status Show space configuration and details. ```bash theme={null} friday workspace status -w my-space ``` | Flag | Description | | ---- | --------------------------- | | `-w` | Space ID or name (required) | ### friday workspace remove Remove a space from the registry. Does **not** delete the directory. ```bash theme={null} friday workspace remove -w my-space --yes ``` **Aliases:** `friday workspace rm`, `friday workspace delete` | Flag | Description | | ------- | --------------------------- | | `-w` | Space ID or name (required) | | `--yes` | Skip confirmation | ### friday workspace cleanup Remove spaces whose directories no longer exist on disk. ```bash theme={null} friday workspace cleanup --yes ``` ## Signals ### friday signal list List configured signals for a space. ```bash theme={null} friday signal list -w my-space friday signal list -w my-space --json ``` **Alias:** `friday sig list` | Flag | Description | | ---- | ---------------- | | `-w` | Space ID or name | ### friday signal trigger Trigger a signal manually. ```bash theme={null} friday signal trigger -n my-signal -w my-space friday signal trigger -n deploy --data '{"branch": "main"}' friday signal trigger -n test --all friday signal trigger -n my-signal -w my-space --stream ``` **Aliases:** `friday signal fire`, `friday signal send` The `--data` flag takes a JSON payload directly — no `{"payload": ...}` envelope needed, the CLI adds it. | Flag | Description | | ----------- | ------------------------------------------ | | `-n` | Signal name (required) | | `--data` | JSON payload | | `-w` | Target space | | `--all` | Trigger across all spaces | | `--exclude` | Space IDs to skip (with `--all`) | | `--stream` | Stream execution events in real time (SSE) | ## Sessions ### friday session list List active and recent sessions. ```bash theme={null} friday session list friday session list --workspace my-space --json ``` **Aliases:** `friday ps`, `friday sesh list` | Flag | Description | | ------------- | -------------------------- | | `--workspace` | Filter by space name or ID | | `--json` | Output as JSON | ### friday session get Get details for a specific session. ```bash theme={null} friday session get sess_abc123 friday session get sess_abc123 --json ``` **Aliases:** `friday session show`, `friday session describe` ### friday session cancel Cancel a running session. ```bash theme={null} friday session cancel sess_abc123 --yes ``` **Aliases:** `friday session kill`, `friday session stop` | Flag | Description | | --------- | ------------------------ | | `--force` | Force cancel | | `--yes` | Skip confirmation prompt | ## Agents See [built-in agents](/core-concepts/agents) for the full list of agents that ship with the platform. ### friday agent list List available agents. ```bash theme={null} friday agent list friday agent list -w my-space --json friday agent list --user ``` | Flag | Description | | -------- | --------------------------------------------------------------- | | `-w` | Filter by space | | `--user` | List user-built agents from the Friday home `agents/` directory | | `--json` | Output as JSON | ### friday agent describe Show detailed information about an agent. ```bash theme={null} friday agent describe -n slack friday agent describe -n my-agent -w my-space ``` **Aliases:** `friday agent show`, `friday agent get` | Flag | Description | | ---- | --------------------- | | `-n` | Agent name (required) | | `-w` | Space ID or name | ### friday agent exec Execute an agent via the Studio and stream results. Useful for testing agents in isolation before wiring them into a workflow. ```bash theme={null} friday agent exec my-agent -i "Summarize the latest PRs" friday agent exec my-agent -i "Check status" --json friday agent exec my-agent -i "Deploy" --env "ENV=staging,TOKEN=xxx" ``` Requires the Studio to be running at `http://localhost:15200`. **Alias:** `friday agent x` | Flag | Description | | --------- | ------------------------------------------------ | | `` | Agent ID (positional, required) | | `-i` | Input prompt (required) | | `--json` | Output raw SSE events as NDJSON | | `--url` | Studio URL (default: `http://localhost:15200`) | | `--env` | Environment variables as `KEY=VALUE,KEY2=VALUE2` | ## Skills ### friday skill list List published skills. ```bash theme={null} friday skill list friday skill list --namespace tempest friday skill list --query "code review" ``` | Flag | Description | | ------------- | ----------------------- | | `--namespace` | Filter by namespace | | `--query` | Search query | | `--all` | Include disabled skills | ### friday skill get Get skill details. ```bash theme={null} friday skill get -n @tempest/pr-code-review ``` | Flag | Description | | ---- | ------------------------------------------------- | | `-n` | Skill name in `@namespace/name` format (required) | ### friday skill publish Publish a skill from a directory containing a `SKILL.md` file. ```bash theme={null} friday skill publish -p /path/to/skill friday skill publish -p . --name @tempest/my-skill ``` **Alias:** `friday skill pub` | Flag | Description | | -------- | ----------------------------------------------- | | `-p` | Path to skill directory (default: `.`) | | `--name` | Override skill name from `SKILL.md` frontmatter | ### friday skill versions List all versions of a skill. ```bash theme={null} friday skill versions -n @tempest/pr-code-review ``` ## Library ### friday library list List stored artifacts and templates. ```bash theme={null} friday library list friday library list --tags csv,reports --limit 20 friday library list --since 2026-01-01 ``` **Alias:** `friday library ls` | Flag | Description | | ------------- | -------------------------------- | | `--tags` | Filter by tags (comma-separated) | | `--since` | Filter by date | | `--limit` | Max results (default: 50) | | `--workspace` | Filter by space path | ### friday library get Get a specific library item by ID (supports partial ID prefix). ```bash theme={null} friday library get abc123 friday library get abc123 --content ``` | Flag | Description | | ----------- | --------------------------- | | `--content` | Include item body in output | ## Artifacts ### friday artifacts list List artifacts by space or chat. ```bash theme={null} friday artifacts list --workspace my-space friday artifacts list --chat chat_abc123 --limit 50 ``` Must provide at least one of `--workspace` or `--chat`. | Flag | Description | | ------------- | -------------------------- | | `--workspace` | Filter by space | | `--chat` | Filter by chat | | `--limit` | Max results (default: 100) | ### friday artifacts get Get a specific artifact by ID. ```bash theme={null} friday artifacts get art_abc123 friday artifacts get art_abc123 --revision 2 ``` | Flag | Description | | ---------------- | ----------------------------------- | | `--revision, -r` | Specific revision (default: latest) | ## Chat and prompts ### friday prompt Send a prompt to the Friday conversation agent. ```bash theme={null} friday prompt "Summarize last week's PRs" friday prompt "Continue from earlier" --chat chat_abc123 friday prompt "Hello" --workspace my-space ``` **Alias:** `friday p` Default output is NDJSON — the `cli-summary` line at the end carries the `chatId` for continuation. | Flag | Description | | ----------------- | ---------------------------------- | | `-w, --workspace` | Scope the chat to a specific space | | `--chat` | Continue an existing chat by ID | | `--human` | Human-readable output | ### friday chat View chat transcripts. Without an ID, lists recent chats. ```bash theme={null} friday chat friday chat chat_abc123 --human friday chat --limit 50 ``` **Alias:** `friday ch` | Flag | Description | | ---------------- | --------------------------------------- | | `--human` | Human-readable output | | `--limit` | Max chats to list (default: 25) | | `--show-prompts` | Include system prompt context in output | ## Logs ### friday logs Query daemon logs with filtering. ```bash theme={null} friday logs --since 5m --level error friday logs --session sess_abc123 --human friday logs --workspace my-space --since 1h ``` Reads logs from the Friday home directory (`global.log` and `workspaces/*.log`). Duration formats: `30s`, `5m`, `1h`. Level can be comma-separated (`error,warn`). **Alias:** `friday log` | Flag | Description | | ------------- | ------------------------------------------------- | | `--since` | Time filter (e.g., `30s`, `5m`, `1h`) | | `--level` | Filter by level: `debug`, `info`, `warn`, `error` | | `--human` | Human-readable output | | `--chat` | Filter by chat ID | | `--session` | Filter by session ID | | `--workspace` | Filter by space ID | ## Daemon ### friday daemon start Start the Friday daemon. ```bash theme={null} friday daemon start --detached friday daemon start --detached --port 18080 ``` **Alias:** `friday daemon run` | Flag | Description | | ------------------ | ------------------------------------------- | | `--port` | Port to listen on (default: 18080) | | `--hostname` | Hostname to bind (default: `127.0.0.1`) | | `--detached` | Run in background | | `--max-workspaces` | Max concurrent space runtimes (default: 10) | | `--idle-timeout` | Idle timeout in seconds (default: 300) | ### friday daemon stop Stop the daemon gracefully. ```bash theme={null} friday daemon stop friday daemon stop --force ``` | Flag | Description | | --------- | ------------------------------------ | | `--force` | Force stop even if spaces are active | ### friday daemon status Check if the daemon is running. ```bash theme={null} friday daemon status friday daemon status --json ``` Exits 1 if the daemon is down — useful as a guard: ```bash theme={null} friday daemon status || friday daemon start --detached ``` ### friday daemon restart Stop, wait 3 seconds, start detached. ```bash theme={null} friday daemon restart friday daemon restart --force ``` ## Global options Most commands support: | Flag | Description | | -------- | ---------------------------------------------- | | `--json` | Output as JSON instead of human-readable text | | `--port` | Target a specific daemon port (default: 18080) | ## Version ```bash theme={null} friday version ``` ## Common patterns ```bash theme={null} # Guard: start daemon if not running friday daemon status || friday daemon start --detached # Send a prompt and continue the conversation CHAT_ID=$(friday prompt "hello" | jq -r 'select(.type=="cli-summary") | .chatId') friday prompt --chat "$CHAT_ID" "follow up" # Fire a signal and stream execution in real time friday signal trigger -n my-signal -w my-space --data '{}' --stream # Correlate logs with a session friday logs --session sess_123 --human --since 5m # Cancel the most recent active session SID=$(friday session list --json | jq -r '[.[]|select(.status=="active")][0].id') [ -n "$SID" ] && friday session cancel "$SID" --yes ``` ## What the CLI can't do Some operations require direct API calls — use `curl` against `http://localhost:18080`: * Create a space from a parsed YAML config — `POST /api/workspaces/create` * Partial space config updates — `PUT/PATCH /api/workspaces/:id/config/signals/:id` * Resource upload or link — `POST /api/workspaces/:id/resources/upload` * Memory reads — `GET /api/memory/:workspaceId/narrative/:memoryName` * Env var management — `PUT /api/config/env` See the [API reference](/api-reference/introduction) for the full endpoint list. # Filesystem Source: https://docs.hellofriday.ai/reference/signals/fs-watch Trigger jobs when files or directories change on disk. Filesystem signals fire when files or directories change under a watched path. Friday uses the OS-native file watcher (inotify on Linux, FSEvents on macOS) — no external dependency, no credentials. ## Config | Field | Type | Required | Default | Description | | ----------- | ------- | -------- | ------- | ----------------------------------------------- | | `path` | string | Yes | — | Absolute or workspace-relative path to watch | | `recursive` | boolean | No | `true` | Watch subdirectories when `path` is a directory | ## Example ```yaml workspace.yml theme={null} signals: artifacts-changed: title: "Artifacts changed" description: "Fires when any file under the artifacts/ directory is modified" provider: fs-watch config: path: "./artifacts" recursive: true ``` ## Path semantics * **Absolute paths** — e.g. `/var/log/app` — are watched as-is. * **Relative paths** — e.g. `./artifacts` or `data/` — are resolved against the space's working directory. * If the path is a **file**, `recursive` is ignored and only that file is watched. * If the path is a **directory**, the default `recursive: true` means every nested file counts; set `recursive: false` to watch only direct children. The watched path must exist when Friday starts. If it's missing, the signal fails to initialize and the space logs a warning — Friday won't create directories for you. ## What triggers the signal The signal fires on any change event the OS surfaces — create, modify, rename, delete. Each event produces one invocation; if you edit three files in quick succession, the job runs three times. OS-level watchers are sensitive to editor temp files (`.swp`, `.tmp`, `~`) and atomic-save patterns that rename a temp file over the target. If your job is noisy, narrow the watched path to a subdirectory that only contains final output. ## Payload The signal payload includes the changed file's path and the event type. If your job needs to read the file, the path is right there: ```yaml workspace.yml theme={null} signals: new-upload: title: "New upload" description: "Process files dropped into the uploads directory" provider: fs-watch config: path: "./uploads" recursive: false schema: type: object properties: path: { type: string } event: { type: string } ``` ## Troubleshooting * Confirm the path exists on disk (`ls `) when Friday starts. * Check OS limits — on Linux, `inotify` has a per-user watcher limit (`/proc/sys/fs/inotify/max_user_watches`). Large recursive watches can exhaust it. * Some network filesystems (NFS, SMB) don't surface OS-level events. Watch a local path instead. Editors often save via "write temp → rename over target", producing two or three events per logical save (create temp, rename, delete temp). Debounce in the job if this causes duplicate work. Friday needs read access to the watched path. For workspace-relative paths, Friday's process owner must own (or have ACL access to) the directory. # HTTP Source: https://docs.hellofriday.ai/reference/signals/http Webhook signals for external services (GitHub, Bitbucket, Jira) and programmatic triggers. HTTP signals receive JSON payloads over POST. The same endpoint serves two use cases: * **Webhooks from external services** (GitHub, Bitbucket, Jira, etc.) delivered through the bundled tunnel * **Programmatic triggers** from the CLI, Studio, or API calls directly against Friday There's no separate "manual" signal type — triggering by hand is just a POST to the HTTP signal's API route. ## Config | Field | Type | Required | Default | Description | | --------- | ------ | -------- | ------- | -------------------------------------------------------------- | | `path` | string | Yes | — | HTTP path for the webhook (method is always POST) | | `timeout` | string | No | — | Max time before the signal handler gives up (e.g. `30s`, `2m`) | ## Example ```yaml workspace.yml theme={null} signals: new-pr: title: "New pull request" description: "Fires on GitHub PR events and manual triggers" provider: http config: path: "/pr" schema: type: object properties: pr_url: { type: string } repo: { type: string } ``` ## Webhook URLs External services deliver events to the bundled tunnel, which forwards them into Friday. The public URL shape is: ``` https://{tunnel-domain}/hook/{provider}/{workspaceId}/{signalId} ``` * `{tunnel-domain}` — your Cloudflare tunnel host (ephemeral, shown in Studio → **Settings** → **Webhook tunnel**) * `{provider}` — determines how the incoming payload is transformed before reaching your job * `{workspaceId}` — the space owning the signal * `{signalId}` — the signal name from `workspace.yml` ### Built-in providers | Provider | Behavior | | ----------- | ---------------------------------------------------------- | | `github` | Extracts `pr_url` from GitHub pull request events | | `bitbucket` | Extracts `pr_url` from Bitbucket pull request events | | `jira` | Extracts `issue_key`, `project_key` from Jira issue events | | `raw` | Forwards the payload as-is (no transformation) | Configure these URLs in the external service's webhook settings (GitHub repo settings, Bitbucket repository webhooks, Jira automation) to wire them into Friday workflows. ## Webhook mappings The tunnel uses a `webhook-mappings.yml` file to decide which events to accept and how to extract signal payload fields from incoming webhook bodies. The image ships with sensible defaults for the starter spaces. Each provider entry defines: * **`event_header`** or **`event_field`** — where to find the event type (HTTP header for GitHub/Bitbucket, body field for Jira) * **`signature_header`** — header used for HMAC-SHA256 verification * **`events`** — map of event names to an optional `actions` filter and a `mapping` of output field to dot-path into the webhook body ```yaml theme={null} providers: github: event_header: x-github-event signature_header: x-hub-signature-256 events: pull_request: actions: [opened, reopened, synchronize] mapping: pr_url: "pull_request.html_url" issues: actions: [opened, labeled] mapping: issue_url: "issue.html_url" issue_key: "issue.number" title: "issue.title" action: "action" push: mapping: ref: "ref" repo: "repository.full_name" sha: "after" pusher: "pusher.name" bitbucket: event_header: x-event-key signature_header: x-hub-signature events: "pullrequest:created": mapping: pr_url: "pullrequest.links.html.href" "pullrequest:updated": mapping: pr_url: "pullrequest.links.html.href" "repo:push": mapping: repo: "repository.full_name" branch: "push.changes[0].new.name" sha: "push.changes[0].new.target.hash" jira: event_field: webhookEvent signature_header: x-hub-signature events: "jira:issue_created": mapping: issue_key: "issue.key" project_key: "issue.fields.project.key" summary: "issue.fields.summary" repo_url: "issue.fields.customfield_10000" "jira:issue_updated": mapping: issue_key: "issue.key" project_key: "issue.fields.project.key" summary: "issue.fields.summary" repo_url: "issue.fields.customfield_10000" ``` ### Customizing mappings Place a custom `webhook-mappings.yml` in the `config/` subdirectory of the Friday home directory (default `~/.friday/local/config/webhook-mappings.yml`). Friday loads it on startup, overriding the built-in defaults. ## Triggering manually The same signal can be fired directly against Friday — no tunnel, no provider prefix. This is how Studio's *Run* button, `friday signal trigger`, and any API caller invoke HTTP signals. ```bash theme={null} curl -X POST http://localhost:18080/api/workspaces//signals/ \ -H 'Content-Type: application/json' \ -d '{ "payload": { "foo": "bar" } }' ``` The request body accepts two fields: | Field | Type | Description | | ---------- | ------ | ------------------------------------------------------ | | `payload` | object | Signal payload — validated against `schema` if present | | `streamId` | string | Optional stream ID for Server-Sent Events progress | The response is an execution handle that streaming clients can attach to. `friday signal trigger -n -w ` is the CLI wrapper for this call. See the [CLI reference](/reference/cli) for the full command shape. ## Payload validation If a signal declares a `schema` block, the payload is validated before the job runs. Invalid requests get a 400 with the specific validation error; the job never starts. ```yaml workspace.yml theme={null} signals: deploy: title: "Deploy" description: "Trigger a deployment" provider: http config: path: "/deploy" schema: type: object required: [environment, version] properties: environment: type: string enum: [staging, production] version: type: string pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$" ``` ## Signature verification For external webhooks, the tunnel verifies HMAC-SHA256 signatures using the `signature_header` defined in the mapping. Set the shared secret on the external service's side; Friday looks it up from space credentials. If signature verification fails, the tunnel returns 401 and the event never reaches Friday. ## Troubleshooting The default development tunnel is ephemeral — it gets a new random URL on every restart. For production, set `TUNNEL_TOKEN` to use a named Cloudflare tunnel with a stable hostname, or point the external service at a real reverse proxy. Check that the event type is in the `events:` list for your provider in `webhook-mappings.yml`. Events not listed are dropped silently. Also check that `actions` (if present) includes the action on the incoming event — e.g. GitHub PR events with `action: closed` are filtered out by the default mapping. Double-check the dot-paths in the mapping — a typo (e.g. `pull_request.url` instead of `pull_request.html_url`) silently resolves to `undefined` and the job sees an empty payload. Mapping errors don't fail the webhook; they just produce empty fields. The shared secret on the external service must match what Friday has stored. For GitHub, set the webhook secret in repo settings and rotate the corresponding Friday credential in the same push. Trailing whitespace in copied secrets is a common cause. # Schedule Source: https://docs.hellofriday.ai/reference/signals/schedule Cron-based signals that fire jobs on a timer. Schedule signals fire on a cron expression, running jobs periodically without any external dependency. Friday evaluates the expression internally — no tunnel, no webhook, no credentials. ## Config | Field | Type | Required | Default | Description | | ---------- | ------ | -------- | ------- | ----------------------------------------------- | | `schedule` | string | Yes | — | Cron expression (standard 5-field syntax) | | `timezone` | string | No | `UTC` | IANA timezone name (e.g. `America/Los_Angeles`) | ## Example ```yaml workspace.yml theme={null} signals: daily-digest: title: "Daily digest" description: "Kick off the morning summary job at 9 AM local time" provider: schedule config: schedule: "0 9 * * *" timezone: "America/Los_Angeles" ``` ## Cron syntax Standard 5-field cron: `minute hour day-of-month month day-of-week`. Ranges (`1-5`), lists (`1,3,5`), steps (`*/15`), and wildcards (`*`) are all supported via the [`cron-parser`](https://www.npmjs.com/package/cron-parser) library. | Expression | Fires | | ------------- | ---------------------------- | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour on the hour | | `0 9 * * *` | 9:00 AM every day | | `0 9 * * 1-5` | 9:00 AM Mon–Fri | | `0 0 1 * *` | Midnight on the 1st of month | | `30 2 * * 0` | 2:30 AM every Sunday | Use any online cron evaluator (e.g. [crontab.guru](https://crontab.guru)) to sanity-check your expression before committing. Invalid expressions fail space validation at load time with a clear error. ## Timezone If `timezone` is omitted, the schedule runs in UTC. Set it to the IANA name of the zone you care about (e.g. `Europe/Warsaw`, `Asia/Tokyo`) — Friday handles DST transitions automatically. Don't use abbreviations like `PST` or `CET` — use full IANA zones like `America/Los_Angeles` or `Europe/Warsaw`. Abbreviations are ambiguous and some libraries reject them. ## Payload Schedule signals fire with an empty payload by default. If you want to pass data into the job, add a `schema` block describing the shape — but remember Friday itself doesn't populate anything, so the job has to read constants or space state. ```yaml workspace.yml theme={null} signals: hourly-refresh: title: "Hourly refresh" description: "Refresh cached data" provider: schedule config: schedule: "0 * * * *" schema: type: object properties: source: type: string default: "cache" ``` ## Troubleshooting Check Friday logs on startup for `invalid cron expression`. If the expression passes parsing but the job still doesn't run, verify the timezone — a schedule of `0 9 * * *` with `UTC` timezone will fire at 9 AM UTC, which might be the middle of the night locally. IANA zones handle DST correctly, but if your space was running during the transition moment, some cron expressions (e.g. `30 2 * * *` in a zone that springs forward past 2:30 AM) can skip or duplicate. Choose a time that doesn't fall in the DST window, or use UTC for timing-sensitive jobs. # workspace.yml reference Source: https://docs.hellofriday.ai/reference/workspace-schema Every field in workspace.yml, with a complete annotated example. `workspace.yml` is the configuration file at the root of every Friday space. It defines agents, jobs, signals, memory stores, and MCP server connections. Friday Studio reads this file at startup and applies it live when you save changes. The annotated example below is drawn from the [GitHub Digest](https://github.com/friday-platform/friday-studio-examples/tree/main/github-digest) starter space — a scheduled job that summarizes open PRs and review requests every Monday and Thursday. ## Complete annotated example ```yaml theme={null} # Schema version. Always "1.0". version: '1.0' workspace: name: GitHub Digest description: Scheduled GitHub PR and review digest every Monday and Thursday # --------------------------------------------------------------------------- # Signals — define what triggers your jobs # --------------------------------------------------------------------------- signals: github-digest-schedule: description: 'Fires every Monday and Thursday at 8:30am Pacific' provider: schedule # schedule | http | fs-watch | slack | telegram | discord | whatsapp | teams config: schedule: '30 8 * * 1,4' # Standard cron expression timezone: America/Los_Angeles # --------------------------------------------------------------------------- # Jobs — define what runs when a signal fires # --------------------------------------------------------------------------- jobs: github-digest-job: description: 'Every Monday and Thursday: summarize open PRs and review requests' triggers: - signal: github-digest-schedule # Must match a key in `signals` fsm: initial: idle states: idle: 'on': github-digest-schedule: target: run run: entry: - type: agent agentId: github-digest-agent # Must match a key in `agents` outputTo: digest-result # Saves agent output as a named artifact - type: emit event: DONE 'on': DONE: target: done done: type: final # --------------------------------------------------------------------------- # Agents — define the AI components jobs can invoke # --------------------------------------------------------------------------- agents: github-digest-agent: type: llm # llm | atlas | user | system description: >- Fetches open PRs and review requests for the authenticated GitHub user, then produces a concise digest grouped by repository. config: provider: anthropic model: claude-sonnet-4-6 prompt: >- You are a GitHub digest assistant. 1. Call `get_me` to find the authenticated user's login. 2. Call `list_pull_requests` filtering for open PRs authored by the user. 3. Call `list_pull_requests` filtering for PRs where review is requested. 4. Group results by repository. For each PR include: title, number, URL, author, and age in days. 5. Produce a concise digest sorted by oldest first within each group. REQUIRED: Call complete() with a summary of how many PRs were found. The job will time out if complete() is not called. tools: - github/get_me # Format: serverId/toolName - github/list_pull_requests # --------------------------------------------------------------------------- # Tools — configure MCP server connections # --------------------------------------------------------------------------- tools: mcp: client_config: timeout: progressTimeout: 2m maxTotalTimeout: 30m servers: github: # Server id — referenced in agent tools lists transport: type: http # http | stdio url: 'https://api.githubcopilot.com/mcp' auth: type: bearer token_env: GH_TOKEN env: GH_TOKEN: from: link # Reads credential from Friday Link (connected account) provider: github key: access_token # --------------------------------------------------------------------------- # Memory — define stores and cross-space mounts # --------------------------------------------------------------------------- memory: own: - name: notes type: short_term # short_term | long_term | scratchpad strategy: narrative - name: memory type: long_term strategy: narrative mounts: - name: user-notes source: user/narrative/notes mode: ro # ro (read-only) | rw (read-write) scope: workspace # workspace | job | agent - name: user-memory source: user/narrative/memory mode: ro scope: workspace ``` ## Field reference ### `version` | Field | Type | Required | Description | | --------- | ------ | -------- | --------------- | | `version` | string | Yes | Always `"1.0"`. | *** ### `workspace` | Field | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------- | | `name` | string | Yes | Display name shown in Friday Studio. | | `description` | string | No | One-line description of what the space does. | *** ### `signals` Each entry under `signals` is a named trigger. The key becomes the signal id referenced in job triggers and FSM states. | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `description` | string | Yes | Human-readable description. | | `provider` | string | Yes | One of: `schedule`, `http`, `fs-watch`, `slack`, `telegram`, `discord`, `whatsapp`, `teams`. (`system` is reserved for internal signals.) | | `config` | object | Yes | Provider-specific config. See [Signal providers](/core-concepts/signals). | **Schedule config:** | Field | Type | Description | | ---------- | ------ | ------------------------------------------------- | | `schedule` | string | Standard 5-field cron expression. | | `timezone` | string | IANA timezone string, e.g. `America/Los_Angeles`. | **HTTP config:** | Field | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------ | | `path` | string | URL path that triggers the webhook, e.g. `/hooks/my-trigger`. The method is always `POST`. | | `timeout` | duration | Optional timeout for signal processing. | *** ### `jobs` Each entry under `jobs` is a named job. Jobs are FSM (finite state machine) definitions that describe which agent runs in which state and what transitions to make. | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------ | | `description` | string | No | Human-readable description. | | `triggers` | array | No | List of `{ signal: signalId }` objects. Omit for jobs invoked only from chat or run manually. | | `fsm` | object | Yes\* | FSM definition. \*A job specifies exactly one of `fsm` (shown here) or `execution`, an agent-pipeline alternative. | | `fsm.initial` | string | Yes | Name of the starting state. | | `fsm.states` | object | Yes | Map of state name to state definition. | **State entry actions:** | Type | Fields | Description | | -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------- | | `agent` | `agentId`, `prompt?`, `outputTo?` | Invoke a configured agent. `outputTo` saves the result as a named artifact. | | `llm` | `provider`, `model`, `prompt`, `tools?`, `outputTo?` | Run an inline LLM step without defining a named agent. | | `emit` | `event` | Emit a named event to drive a state transition. | | `notification` | `message`, `communicators?` | Send a message to configured communicators (Slack, Telegram, …). | *** ### `agents` Each entry under `agents` is a named agent. The key is the `agentId` referenced in job FSM entry actions. | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Yes | One of: `llm`, `atlas`, `user`, `system`. | | `description` | string | Yes | Human-readable description. | | `config` | object | Varies | Agent-specific config. Required for `llm`; optional for `system`; `atlas` and `user` take top-level `agent`/`prompt` instead. | **LLM agent config:** | Field | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `provider` | string | Yes | Inference provider. Common values: `anthropic`, `openai`, `gemini`, `groq`. | | `model` | string | Yes | Model identifier, e.g. `claude-sonnet-4-6`. | | `prompt` | string | Yes | System prompt for the agent. | | `tools` | array | No | MCP tool allowlist in `serverId/toolName` format. Omit to allow all tools from all enabled servers. | **Atlas agent config:** | Field | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------------------------------------------------------- | | `agent` | string | Yes | Atlas Agent ID from the registry, e.g. `web`, `gh`, `slack`, `hubspot`. | | `prompt` | string | Yes | Task-specific context layered on the bundled agent's behavior. | | `config` | object | No | Agent-specific configuration passed to the agent. | | `env` | object | No | Environment variables. Values can be literals or a Link reference (`{ from: link, … }`). | *** ### `tools.mcp` | Field | Type | Description | | --------------------------------------- | -------- | ---------------------------------------------------------------------- | | `client_config.timeout.progressTimeout` | duration | How long to wait between tool call progress updates before timing out. | | `client_config.timeout.maxTotalTimeout` | duration | Maximum total time for a single tool call. | | `servers` | object | Map of server id to server config. | **Server config:** | Field | Type | Description | | ------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transport.type` | string | `http` or `stdio`. | | `transport.url` | string | For `http` transport: the MCP server URL. | | `transport.command` | string | For `stdio` transport: the command to run. | | `transport.args` | array | For `stdio` transport: command arguments. | | `auth.type` | string | `bearer` (the only supported value). Omit the entire `auth` block for servers that need no auth. | | `auth.token_env` | string | Name of the env var holding the bearer token. | | `env` | object | Env var definitions. Values can be literals or a Link reference: `{ from: link, key, … }` with either `provider` (e.g. `github`) or a specific credential `id`. | *** ### `memory` | Field | Type | Description | | -------- | ----- | ------------------------------------------------------------------------------------- | | `own` | array | Memory stores owned by this space. Each entry: `{ name, type, strategy? }`. | | `mounts` | array | Cross-space memory mounts. Each entry: `{ name, source, mode, scope, scopeTarget? }`. | **Own store fields:** | Field | Values | Description | | ---------- | --------------------------------------- | ------------------------------------------------------------------------------- | | `type` | `short_term`, `long_term`, `scratchpad` | Retention policy. | | `strategy` | `narrative` | How entries are written and retrieved. Optional; `narrative` is the only value. | **Mount fields:** | Field | Values | Description | | ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `source` | string | `{spaceId}/narrative/{storeName}` path to the source store. | | `mode` | `ro`, `rw` | Read-only or read-write access. Defaults to `ro`. | | `scope` | `workspace`, `job`, `agent` | How broadly the mount is visible within this space. | | `scopeTarget` | string | **Required** when `scope` is `job` or `agent` — the id of the job or agent to scope the mount to. Omit for `scope: workspace`. | *** ## More examples Browse the [Friday Studio Examples](https://github.com/friday-platform/friday-studio-examples) repo for a dozen ready-made spaces with complete `workspace.yml` files covering email, GitHub, Telegram, Google Sheets, price monitoring, and more. # Generate text and objects Source: https://docs.hellofriday.ai/sdk/guides/call-llms Route LLM calls through Friday's provider registry instead of importing API clients directly. ## Basic generation Use `ctx.llm.generate()` for text completion: ```python theme={null} from friday_agent_sdk import agent, ok @agent(id="writer", version="1.0.0", description="Writes documentation") def execute(prompt, ctx): result = ctx.llm.generate( messages=[{"role": "user", "content": f"Write docs for: {prompt}"}], model="anthropic:claude-sonnet-4-6", ) return ok({"output": result.text}) ``` ## Model Resolution You can specify models in three ways: ```python theme={null} # Fully qualified - uses this exact model ctx.llm.generate(..., model="anthropic:claude-sonnet-4-6") # Bare model name with decorator provider - resolves automatically @agent(..., llm={"provider": "anthropic", "model": "claude-sonnet-4-6"}) def execute(prompt, ctx): # No model arg - uses decorator default result = ctx.llm.generate(...) # Override per-call result = ctx.llm.generate(..., model="claude-haiku-4-5") ``` Resolution order (first match wins): 1. Fully qualified per-call (`provider:model`) - use directly 2. Bare per-call + decorator provider - resolve 3. No per-call model + decorator default - use default 4. Nothing configured - error ## Structured Output Use `ctx.llm.generate_object()` for JSON Schema-constrained output: ```python theme={null} schema = { "type": "object", "properties": { "title": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}, "complexity": {"type": "string", "enum": ["low", "medium", "high"]}, }, "required": ["title", "complexity"], } result = ctx.llm.generate_object( messages=[{"role": "user", "content": "Analyse this task"}], schema=schema, model="anthropic:claude-sonnet-4-6", ) # result.object contains the parsed JSON data = result.object ``` ## Response Fields The `LlmResponse` object contains: ```python theme={null} result.text # Generated text (None for generate_object) result.object # Structured output dict (None for generate) result.model # Model identifier used (e.g., "anthropic:claude-sonnet-4-6") result.usage # {"input_tokens": 120, "output_tokens": 250} result.finish_reason # "stop", "length", etc. ``` ## Error Handling LLM errors raise `LlmError`: ```python theme={null} from friday_agent_sdk import LlmError, agent, err, ok @agent(id="retry-agent", version="1.0.0", description="Retries on failure") def execute(prompt, ctx): try: result = ctx.llm.generate(..., model="expensive-model") except LlmError as e: # Fallback to cheaper model result = ctx.llm.generate(..., model="claude-haiku-4-5") return ok({"output": result.text}) ``` ## Advanced Options ```python theme={null} result = ctx.llm.generate( messages=[...], model="anthropic:claude-sonnet-4-6", max_tokens=2000, # Limit response length temperature=0.7, # Sampling temperature (0-2) provider_options={ # Provider-specific passthrough "anthropic": { "thinking": {"type": "enabled", "budgetTokens": 4000}, }, }, ) ``` Full API reference for LLM generation. Why the SDK uses host capabilities instead of direct imports. # Extract structured data Source: https://docs.hellofriday.ai/sdk/guides/handle-structured-input Extract JSON configurations from Friday's enriched prompts using parse_input() and parse_operation(). ## The problem Friday sends your agent an "enriched prompt" - a markdown string containing: * The user's task * Temporal facts (current time, relevant history) * Signal data (HTTP request body, cron metadata) * Accumulated context from previous steps Deterministic code agents cannot parse this like LLMs do. The SDK provides extraction utilities. ## Simple JSON Extraction Use `parse_input()` to extract a JSON object from the prompt: ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import agent, ok, parse_input @dataclass class Config: repository: str branch: str dry_run: bool = False @agent(id="git-agent", version="1.0.0", description="Git operations") def execute(prompt, ctx): # Extracts JSON and validates against Config dataclass config = parse_input(prompt, Config) # config.repository, config.branch, config.dry_run available return ok({ "repo": config.repository, "branch": config.branch, }) ``` The prompt might contain: ````markdown theme={null} Task: Deploy the application ```json { "repository": "my-org/app", "branch": "main", "dry_run": true } ``` ```` ## Extraction Strategy `parse_input()` searches in this order: 1. **Raw JSON objects** - Scans for balanced-brace JSON objects 2. **Code-fenced blocks** - Extracts from ` ```json ... ``` ` 3. **Full prompt** - Attempts to parse the entire prompt as JSON Unknown keys are filtered when using a dataclass schema, preventing enrichment context from crashing construction. ## Discriminated Operations When your agent handles multiple operations, use `parse_operation()`: ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import agent, err, ok, parse_operation @dataclass class CloneConfig: operation: str # Must be "clone" repository: str branch: str = "main" @dataclass class DeployConfig: operation: str # Must be "deploy" environment: str version: str OPERATIONS = { "clone": CloneConfig, "deploy": DeployConfig, } @agent(id="ops-agent", version="1.0.0", description="Git operations") def execute(prompt, ctx): try: config = parse_operation(prompt, OPERATIONS) except ValueError as e: return err(f"Invalid operation: {e}") match config.operation: case "clone": return _handle_clone(config) case "deploy": return _handle_deploy(config) case _: return err(f"Unknown operation: {config.operation}") ``` ## Plain Dict Extraction Without a dataclass, get a plain dict: ```python theme={null} # Returns dict data = parse_input(prompt) # Access fields task = data.get("task") priority = data.get("priority", "medium") ``` ## Validation Errors When using dataclasses, missing required fields produce clear errors: ```python theme={null} @dataclass class StrictConfig: required_field: str another_required: int config = parse_input('{"required_field": "value"}', StrictConfig) # Raises: ValueError: JSON parsed but doesn't match StrictConfig: missing {'another_required'} ``` ## Real Example: Jira Agent ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import agent, err, ok, parse_operation @dataclass class IssueViewConfig: operation: str issue_key: str @dataclass class IssueSearchConfig: operation: str jql: str max_results: int = 50 @dataclass class IssueCreateConfig: operation: str project_key: str summary: str description: str | None = None issue_type: str = "Bug" OPERATION_SCHEMAS = { "issue-view": IssueViewConfig, "issue-search": IssueSearchConfig, "issue-create": IssueCreateConfig, } @agent(id="jira", version="1.0.0", description="Jira operations") def execute(prompt, ctx): try: config = parse_operation(prompt, OPERATION_SCHEMAS) except ValueError as e: return err(str(e)) match config.operation: case "issue-view": return _view_issue(config, ctx) case "issue-search": return _search_issues(config, ctx) case "issue-create": return _create_issue(config, ctx) ``` ## When to Use Which | Function | Use When | | ---------------------------------- | --------------------------------------------------- | | `parse_input(prompt)` | Single configuration, no discriminated types | | `parse_input(prompt, Schema)` | Single configuration, want typed validation | | `parse_operation(prompt, schemas)` | Multiple operations via `"operation"` discriminator | Full API reference for parse\_input() and parse\_operation(). # Fetch HTTP APIs Source: https://docs.hellofriday.ai/sdk/guides/make-http-requests Make outbound HTTP requests through Friday's fetch layer - TLS, timeouts, and audit logging handled by Friday. ## Basic GET request ```python theme={null} from friday_agent_sdk import agent, ok, err @agent(id="fetcher", version="1.0.0", description="Fetches data from APIs") def execute(prompt, ctx): response = ctx.http.fetch("https://api.example.com/data") if response.status >= 400: return err(f"API error {response.status}") data = response.json() # Convenience helper return ok({"data": data}) ``` ## POST with JSON Body ```python theme={null} import json response = ctx.http.fetch( "https://api.example.com/items", method="POST", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {ctx.env['API_KEY']}", }, body=json.dumps({"name": "New Item", "value": 42}), ) ``` Access environment variables via `ctx.env` - configure them in the decorator: ```python theme={null} @agent( id="api-client", version="1.0.0", description="Calls external API", environment={ "required": [ {"name": "API_KEY", "description": "API authentication token"}, ], }, ) def execute(prompt, ctx): api_key = ctx.env["API_KEY"] # Raises KeyError if not set ... ``` ## Response Handling ```python theme={null} response = ctx.http.fetch(...) # Fields response.status # HTTP status code (int) response.headers # Dict of response headers response.body # Response body as string # Convenience methods data = response.json() # Parses body as JSON ``` ## Error Handling HTTP errors raise `HttpError`: ```python theme={null} from friday_agent_sdk import HttpError, agent, err, ok @agent(id=" resilient", version="1.0.0", description="Handles API failures") def execute(prompt, ctx): try: response = ctx.http.fetch("https://api.example.com/data") except HttpError as e: # Network-level failure (DNS, TLS, timeout) return err(f"Request failed: {e}") if response.status >= 500: # Server error - could retry return err(f"Server error: {response.status}") if response.status == 404: # Not found - might be expected return ok({"found": False}) return ok({"found": True, "data": response.json()}) ``` ## Timeouts ```python theme={null} response = ctx.http.fetch( "https://slow-api.example.com/data", timeout_ms=30000, # 30 seconds ) ``` ## Methods and Options ```python theme={null} response = ctx.http.fetch( url, method="PUT", # GET, POST, PUT, PATCH, DELETE, HEAD headers={...}, # Dict of request headers body="raw body", # String body timeout_ms=10000, # Request timeout ) ``` ## Limitations * **5MB response limit** - Matches Friday's platform webfetch limit * **No URL allowlists yet** - Designed but not implemented; all outbound requests allowed * **No streaming responses** - Body returned as complete string Full API reference for HTTP fetch. For APIs with MCP servers, tools may be simpler than raw HTTP. # Emit progress events Source: https://docs.hellofriday.ai/sdk/guides/stream-progress Emit progress events that appear in Friday's UI during long-running operations. ## Basic progress emission ```python theme={null} from friday_agent_sdk import agent, ok @agent(id="long-task", version="1.0.0", description="Takes a while") def execute(prompt, ctx): ctx.stream.progress("Starting analysis...") # Do work... result = ctx.llm.generate(...) ctx.stream.progress("Processing results...") # More work... data = process(result.text) ctx.stream.progress("Complete!") return ok({"data": data}) ``` ## Intent emission Emit high-level intents for significant state changes: ```python theme={null} ctx.stream.intent("Analyzing repository structure") # Walk directory tree... ctx.stream.intent("Identifying issues") # Run analysis... ctx.stream.intent("Generating report") ``` ## With Tool Context Associate progress with specific tools: ```python theme={null} ctx.stream.progress("Fetching repository data", tool_name="GitHub") # Call GitHub MCP tools... ctx.stream.progress("Analyzing code patterns", tool_name="Analyzer") # LLM analysis... ctx.stream.progress("Creating summary", tool_name="Reporter") ``` ## Real Example: Multi-Phase Agent ```python theme={null} from friday_agent_sdk import agent, ok, AgentExtras @agent(id="analyzer", version="1.0.0", description="Multi-phase analysis") def execute(prompt, ctx): # Phase 1: Extract parameters ctx.stream.progress("Parsing request") params = extract_params(prompt) # Phase 2: LLM preprocessing ctx.stream.progress("Running initial analysis", tool_name="LLM") analysis = ctx.llm.generate( messages=[{"role": "user", "content": f"Analyze: {params}"}], model="claude-haiku-4-5", ) # Phase 3: Tool calls ctx.stream.progress("Fetching related data", tool_name="GitHub") issues = ctx.tools.call("search_issues", {"query": params["query"]}) # Phase 4: Synthesis ctx.stream.progress("Synthesizing results", tool_name="Synthesizer") result = synthesize(analysis.text, issues) ctx.stream.progress("Analysis complete") return ok({ "summary": result["summary"], "recommendations": result["recommendations"], }) ``` ## When to Emit Emit progress when: * Starting a distinct phase of work * Before expensive operations (LLM calls, HTTP requests) * After completing significant milestones * When handling fallback scenarios ("Retrying with different model...") Do not emit: * In tight loops (debounce or batch instead) * For trivial operations (\< 100ms) * Excessively verbose detail ("Step 1 of 50", "Step 2 of 50"...) ## Emission during LLM calls Emit progress before expensive operations - `ctx.stream.progress()` is fire-and-forget over NATS and does not block the handler: ```python theme={null} ctx.stream.progress("Starting LLM call...") # progress event already sent to connected clients result = ctx.llm.generate(messages, model="claude-sonnet-4-6") ctx.stream.progress("LLM complete, processing...") ``` ## Raw Event Emission For custom event types, use `emit()`: ```python theme={null} ctx.stream.emit("custom-event", {"phase": "validation", "count": 42}) ``` The `data` parameter accepts either a dict (JSON-serialized) or string. Full stream capability API reference The subprocess model and host capabilities # Call MCP tools Source: https://docs.hellofriday.ai/sdk/guides/use-mcp-tools Configure MCP servers in your agent decorator and call tools via ctx.tools. ## Configure an MCP server Declare required MCP servers in the `@agent` decorator: ```python theme={null} from friday_agent_sdk import agent, ok @agent( id="github-helper", version="1.0.0", description="Uses GitHub MCP server", mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], } } }, ) def execute(prompt, ctx): ... ``` ## List Available Tools ```python theme={null} tools = ctx.tools.list() for tool in tools: print(f" {tool.name}: {tool.description}") print(f" Schema: {tool.input_schema}") ``` Returns a list of `ToolDefinition` objects: ```python theme={null} tool.name # Tool identifier tool.description # Human-readable description tool.input_schema # JSON Schema for arguments ``` ## Call a Tool ```python theme={null} result = ctx.tools.call( "search_issues", { "query": "is:open label:bug", "repo": "my-org/my-repo", }, ) # result is a dict parsed from the tool's JSON output return ok({"issues": result["issues"]}) ``` ## Error Handling Tool failures raise `ToolCallError`: ```python theme={null} from friday_agent_sdk import ToolCallError, agent, err, ok @agent(id="safe-caller", version="1.0.0", description="Handles tool errors") def execute(prompt, ctx): try: result = ctx.tools.call("risky_operation", {"data": prompt}) except ToolCallError as e: return err(f"Tool failed: {e}") return ok({"result": result}) ``` ## Real Example: Time Operations ```python theme={null} @agent( id="time-agent", version="1.0.0", description="Time conversion agent", mcp={ "time": { "transport": { "type": "stdio", "command": "uvx", "args": ["mcp-server-time", "--local-timezone", "UTC"], } } }, ) def execute(prompt, ctx): # Get current time in Tokyo result = ctx.tools.call( "get_current_time", {"timezone": "Asia/Tokyo"}, ) # Convert time converted = ctx.tools.call( "convert_time", { "source_timezone": "UTC", "time": "14:30", "target_timezone": "America/New_York", }, ) return ok({ "tokyo_time": result, "converted": converted, }) ``` ## Multiple MCP Servers ```python theme={null} @agent( id="multi-tool", version="1.0.0", description="Uses GitHub and database tools", mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], } }, "postgres": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres"], "env": { "DATABASE_URL": "postgresql://...", }, } } }, ) def execute(prompt, ctx): tools = ctx.tools.list() # Tools from both servers are available github_tools = [t for t in tools if "github" in t.name] db_tools = [t for t in tools if "postgres" in t.name] ... ``` ## Environment Variables in MCP Pass environment variables directly to MCP server processes: ```python theme={null} mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "your-github-token", } } } } ``` To declare environment variables your agent code accesses via `ctx.env`, use the `environment` decorator field: ```python theme={null} environment={ "required": [ {"name": "GITHUB_TOKEN", "description": "GitHub API token"}, ] } ``` ## Stdio vs SSE Transport Currently only `stdio` transport is supported. `sse` (Server-Sent Events) is planned. ## Tool Chaining ```python theme={null} # Chain multiple tool calls tools = ctx.tools.list() # Find relevant tool by name search_tool = next((t for t in tools if t.name == "search_issues"), None) if not search_tool: return err("search_issues tool not available") # Search issues = ctx.tools.call("search_issues", {"query": prompt}) # For each issue, get details for issue in issues["issues"][:5]: details = ctx.tools.call( "get_issue", {"owner": "my-org", "repo": "my-repo", "issue_number": issue["number"]}, ) # Process details... ``` Full tools capability API reference Official MCP protocol documentation Browse available MCP servers # How agents work Source: https://docs.hellofriday.ai/sdk/how-agents-work The subprocess model, host capabilities, and registration flow. Your agent runs as a Python subprocess spawned per invocation. The daemon sends it one request, the agent handles it and exits. Here's what that means in practice. **What this gives you:** * **No API keys in your code** - Friday manages providers and authentication * **Secure external access** - HTTP and tools route through Friday's audited channels * **Simple development** - write a Python file, register it, it runs immediately * **No dependency conflicts** - `pip install` additional pure-Python packages into the agent environment **How you work with it:** * Write normal Python using the `@agent` decorator and `execute()` function * Access the outside world through `ctx.llm`, `ctx.http`, `ctx.tools`, `ctx.stream` * Register with the daemon - no restart required ## Execution model Each invocation spawns a fresh process. The daemon sends one NATS message, the agent handles it, returns a result, and exits. State does not persist between calls - if you need to store state, write to memory via MCP tools or return it in the result. ```python agent.py theme={null} from friday_agent_sdk import agent, ok, AgentContext, run @agent(id="my-agent", version="1.0.0", description="Does something useful") def execute(prompt: str, ctx: AgentContext): # prompt: the task passed from the job FSM # ctx: bridges to Friday's capabilities return ok({"result": "done"}) if __name__ == "__main__": run() # connects to NATS and handles the request ``` The `if __name__ == "__main__": run()` block is required - it's how the subprocess connects to the daemon. ## Host capabilities Your agent calls Friday for any external interaction: | Capability | What you use it for | Why Friday handles it | | ------------ | -------------------------------- | ---------------------------------------------- | | `ctx.llm` | Generate text or structured data | API keys, rate limits, provider routing | | `ctx.http` | Call external APIs | TLS, timeouts, audit logging | | `ctx.tools` | Invoke MCP servers | External processes run outside the agent | | `ctx.stream` | Show progress in the UI | Real-time updates to connected clients | | `ctx.env` | Read configured secrets | No host environment access from the subprocess | Route I/O through `ctx` for production work. This keeps credentials out of your code, enables audit logging, and lets Friday handle rate limits and provider routing. You can install `requests` or `anthropic` directly for local debugging, but host capabilities are preferred for anything that runs in a space. ## Registration Register an agent from anywhere on your machine: ```bash theme={null} friday agent register ./my-agent ``` The daemon validates the agent (spawns it with `FRIDAY_VALIDATE_ID`, reads metadata via NATS), copies all source files from the same directory to the Friday home directory under `agents/{id}@{version}/`, and hot-reloads the registry. No daemon restart required. Once registered, reference the agent by ID in `workspace.yml`: ```yaml workspace.yml theme={null} agents: my-agent: type: user description: "Does something useful" ``` ## Current constraints **Dependencies** * The `friday_agent_sdk` and Python standard library are available by default * You can `pip install` additional packages - native C extensions (NumPy, Pydantic) work if the environment has them * All I/O should still route through `ctx.llm`, `ctx.http`, `ctx.tools` for audit logging and credential management **State** * Every invocation is a fresh process - module-level state resets each time * Persist through MCP tools or return data in `ok()` **One agent per file** * One `@agent` decorator per Python file. A second raises `RuntimeError`. Split into separate files. ## Iteration workflow Edit your agent source and re-register: ```bash theme={null} vim my-agent/agent.py friday agent register ./my-agent ``` Test with the [Agent Tester](/guides/agent-tester) or via the CLI: ```bash theme={null} friday agent exec my-agent -i "test input" ``` ## See also Step-by-step walkthrough of building your first agent. Complete API documentation for the SDK. * [Quickstart](/sdk/quickstart) — Step-by-step walkthrough of building your first agent. * [Python reference](/sdk/python-reference/agent-decorator) — Complete API documentation for the SDK. # Agent SDK Source: https://docs.hellofriday.ai/sdk/overview Build custom agents that integrate with Friday's runtime and tool ecosystem. Friday ships with [built-in agents](/core-concepts/agents) for common integrations (GitHub, Jira, Slack, etc.), but your business has unique APIs, workflows, and domain logic. The Agent SDK lets you wrap those into custom agents the platform can orchestrate alongside built-ins in [jobs](/core-concepts/jobs). ## How it works Your agent runs as a Python subprocess managed by Friday. It communicates with the outside world through the host: Route calls through Friday's provider registry. No API keys in your code - the platform handles auth, rate limits, and routing. Call external APIs through Friday's fetch layer. TLS termination and timeouts handled by Friday. Configure Model Context Protocol servers and invoke tools - GitHub, databases, filesystems, and more. Emit real-time progress updates that appear in the Studio UI while your agent runs. The subprocess model keeps credentials out of your code. All I/O flows through Friday. The platform manages credentials, rate limits, and audit logging. ## Language SDKs Define agents in code using the Agent SDK. Each SDK provides a consistent interface for building agents that integrate with Friday's runtime, tools, and orchestration layer. Build agents in Python. Runtime Python package with NATS subprocess execution. Install additional pure-Python packages into the agent environment. Set up [IDE support](/sdk/python#development-setup) for autocomplete and type checking. Python is available today and more languages are coming soon. Don't see your language? [Reach out](/contact) to request an SDK for a specific language. # Overview Source: https://docs.hellofriday.ai/sdk/python Write Friday agents in Python using the friday-agent-sdk package. Write agents in Python - the Friday platform handles the rest. Your agent runs as a Python subprocess with access to LLMs, HTTP, and MCP tools through the `ctx` interface. ## How it works Your agent is a Python function with two inputs and one output: ```python theme={null} def execute(prompt: str, ctx: AgentContext) -> Result: # prompt: what the job FSM passed as input # ctx: bridges to Friday's capabilities (LLMs, HTTP, MCP tools) # Result: structured data back to the platform ``` The `@agent` decorator registers your function with Friday. Call `run()` in the `__main__` block - that's how the subprocess connects to the daemon. ## Quick example ```python agent.py theme={null} from friday_agent_sdk import agent, ok, AgentContext, run @agent( id="my-agent", version="1.0.0", description="Summarizes text with an LLM", ) def execute(prompt: str, ctx: AgentContext): result = ctx.llm.generate( messages=[{"role": "user", "content": f"Summarize this: {prompt}"}], model="anthropic:claude-haiku-4-5", ) return ok({"summary": result.text}) if __name__ == "__main__": run() ``` The `@agent` decorator registers metadata. Host capabilities on `ctx` handle LLM calls, HTTP requests, MCP tools, and streaming. `ok()` returns structured data to the platform. ## Development setup Install the SDK locally for IDE support. The SDK is a Python package - Friday runs your agent as a subprocess, but you need the package locally for autocomplete and type checking. **1. Clone the SDK repository:** ```bash theme={null} git clone git@github.com:friday-platform/agent-sdk.git ~/agent-sdk ``` **2. Create a virtual environment and install the SDK:** ```bash theme={null} cd my-agent-project uv venv source .venv/bin/activate # or: .venv\Scripts\activate on Windows uv pip install -e ~/agent-sdk/packages/python ``` **3. Configure VS Code:** Create `.vscode/settings.json` in your agent project: ```json theme={null} { "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.analysis.typeCheckingMode": "basic", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true } } ``` Install the [Ruff extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) for linting and formatting. Reload VS Code after creating the settings. **Verify:** Open any `.py` file and check that `from friday_agent_sdk import agent` shows no import errors. **Let an agent write your agent.** The [`writing-friday-python-agents`](https://github.com/friday-platform/agent-sdk/tree/main/packages/python/skills/writing-friday-python-agents) skill works in Claude Code and other coding agents. It covers the full SDK, so agents you generate register cleanly the first time. ## Prerequisites * Python 3.11+ (for IDE support and local testing) * A running Friday daemon (`friday daemon status`) * An Anthropic API key ## Get started Build a text analysis agent from scratch in under 10 minutes. The subprocess model, host capabilities, and registration. ## Guides Models, structured output, error handling. External API calls through the fetch layer. GitHub, databases, and other MCP servers. Parse JSON from enriched prompts. Real-time UI updates during execution. ## Reference Metadata, environment, MCP, and LLM configuration. Execution context and capability availability. LLM generation methods and response types. HTTP fetch and response handling. MCP tool listing and invocation. Progress and intent emission. ok(), err(), and AgentExtras. parse\_input() and parse\_operation(). # AgentContext Source: https://docs.hellofriday.ai/sdk/python-reference/agent-context Execution context passed to agent handlers, providing access to environment, capabilities, and metadata. ## Definition ```python theme={null} @dataclass class AgentContext: env: dict[str, str] = field(default_factory=dict) config: dict = field(default_factory=dict) session: SessionData | None = None output_schema: dict | None = None tools: Tools = field(default_factory=_uninitialized_tools) llm: Llm = field(default_factory=_uninitialized_llm) http: Http = field(default_factory=_uninitialized_http) stream: StreamEmitter = field(default_factory=_uninitialized_stream) ``` Capability fields (`tools`, `llm`, `http`, `stream`) are always non-None. They default to safe stubs that raise `RuntimeError` if called outside the host environment. ## Fields ### `env` * **Type:** `dict[str, str]` * **Description:** Environment variables configured via the `@agent` decorator's `environment` field. ```python theme={null} api_key = ctx.env["ANTHROPIC_API_KEY"] # Raises KeyError if not set debug = ctx.env.get("DEBUG", "false") # Safe access with default ``` Populated from Friday's environment matching `environment.required` and `environment.optional` configuration. ### `config` * **Type:** `dict` * **Description:** Agent-specific configuration and space context. May include: * `platformUrl` - Friday API base URL * `skills` - List of space skills for the session * `workDir` - Existing space directory (if FSM set one up) * Custom fields passed by the orchestrator ```python theme={null} platform_url = ctx.config.get("platformUrl", "http://localhost:18080") skills = ctx.config.get("skills", []) ``` ### `session` * **Type:** `SessionData | None` * **Description:** Session metadata when running within a Friday session. ```python theme={null} @dataclass class SessionData: id: str # Session identifier workspace_id: str # Space identifier user_id: str # User identifier datetime: str # ISO format timestamp ``` May be `None` in test contexts or standalone execution. ### `output_schema` * **Type:** `dict | None` * **Description:** JSON Schema for structured output, if specified by the caller. ```python theme={null} if ctx.output_schema: # Use structured generation result = ctx.llm.generate_object(..., schema=ctx.output_schema) else: # Standard text generation result = ctx.llm.generate(...) ``` ### `tools` * **Type:** `Tools` * **Description:** MCP tool capability wrapper. Always available - returns empty list when no MCP servers configured. Methods: * `ctx.tools.list()` → `list[ToolDefinition]` * `ctx.tools.call(name, args)` → `dict` See [ctx.tools](/sdk/python-reference/tools-capability). ### `llm` * **Type:** `Llm` * **Description:** LLM capability wrapper for generation calls. Always available in host environment. Methods: * `ctx.llm.generate(messages, model, ...)` → `LlmResponse` * `ctx.llm.generate_object(messages, schema, ...)` → `LlmResponse` See [ctx.llm](/sdk/python-reference/llm-capability). ### `http` * **Type:** `Http` * **Description:** HTTP capability wrapper for outbound requests. Always available in host environment. Methods: * `ctx.http.fetch(url, method, headers, body, timeout_ms)` → `HttpResponse` See [ctx.http](/sdk/python-reference/http-capability). ### `stream` * **Type:** `StreamEmitter` * **Description:** Stream capability for progress emission. Always available in host environment. Methods: * `ctx.stream.progress(content, tool_name)` * `ctx.stream.intent(content)` * `ctx.stream.emit(event_type, data)` See [ctx.stream](/sdk/python-reference/stream-capability). ## Availability Guarantees | Field | Guaranteed | Notes | | --------------- | ---------- | ------------------------------------------------------ | | `env` | Yes | Empty dict if no environment configured | | `config` | Yes | Empty dict if no config provided | | `session` | No | May be None outside Friday sessions | | `output_schema` | No | Only when caller specifies schema | | `tools` | Yes | Always present; returns empty list if no MCP servers | | `llm` | Yes | Always present; raises RuntimeError if not initialized | | `http` | Yes | Always present; raises RuntimeError if not initialized | | `stream` | Yes | Always present; safe no-op in test contexts | ## Defensive Programming ```python theme={null} from friday_agent_sdk import agent, ok @agent(id="safe", version="1.0.0", description="Handles missing capabilities") def execute(prompt, ctx): # Safe access with fallbacks api_key = ctx.env.get("OPTIONAL_KEY") # Returns None if missing # Required access with check if "REQUIRED_KEY" not in ctx.env: return err("REQUIRED_KEY not set. Connect in Friday Link.") # Capability availability - capabilities are always present # They raise RuntimeError if called outside the host environment if ctx.output_schema: # Structured path result = ctx.llm.generate_object(...) else: # Standard path result = ctx.llm.generate(...) # Progress emission - always safe to call ctx.stream.progress("Working...") return ok({"result": result.text}) ``` **Test contexts:** When running outside Friday (unit tests), calling capabilities raises `RuntimeError` with a clear message. To test agents properly, mock the capabilities or run against a local Friday daemon. ## Context Round-Trip All context fields are serialized over NATS for each invocation: 1. Friday serializes context as JSON 2. JSON is sent to the agent subprocess via NATS 3. SDK bridge deserializes to `AgentContext` dataclass 4. Your code uses the context 5. Result serializes back to the host The `context-inspector` example agent demonstrates all fields survive this round-trip correctly. ## See Also LLM generation HTTP requests MCP tool calls Progress streaming ok() and err() return values # @agent Decorator Source: https://docs.hellofriday.ai/sdk/python-reference/agent-decorator Registers a function as a Friday agent with metadata for discovery and execution. ## Signature ```python theme={null} @agent( *, id: str, version: str, description: str, display_name: str | None = None, summary: str | None = None, constraints: str | None = None, examples: list[str] | None = None, input_schema: type | None = None, output_schema: type | None = None, environment: dict[str, Any] | None = None, mcp: dict[str, Any] | None = None, llm: dict[str, Any] | None = None, use_workspace_skills: bool = False, ) def execute(prompt: str, ctx: AgentContext) -> OkResult | ErrResult: ... ``` ## Required Parameters The build API validates that all three are present. Missing any returns HTTP 400 with `"phase": "validate"`. ### `id` * **Type:** `str` * **Description:** Unique identifier for the agent. Use kebab-case. * **Constraints:** Must be unique within your space. * **Example:** `"text-analyzer"`, `"github-helper"`, `"jira-operations"` ### `version` * **Type:** `str` * **Description:** Semantic version of the agent. * **Example:** `"1.0.0"`, `"2.1.0-alpha.1"` * **Behaviour:** Multiple versions coexist on disk; Friday resolves the ID to the highest semver version. ### `description` * **Type:** `str` * **Description:** What the agent does. Used by the planner for delegation decisions. * **Guidance:** Be specific about capabilities and use cases. 50-200 characters. * **Required:** Build fails without this field. ## Optional Parameters ### `display_name` * **Type:** `str | None` * **Description:** Human-readable name for the UI. Falls back to `id` if not provided. ### `summary` * **Type:** `str | None` * **Description:** One-line summary for agent listings. ### `constraints` * **Type:** `str | None` * **Description:** Limitations, requirements, or conditions for using the agent. * **Example:** `"Requires GitHub token. Cannot access space database tables."` ### `examples` * **Type:** `list[str] | None` * **Description:** Example prompts that trigger this agent. Helps the planner learn delegation patterns. ```python theme={null} examples=[ "Write a Python function to parse JSON", "Debug this error in the codebase", "Analyse stack traces and identify root causes", ] ``` ### `input_schema` * **Type:** `type | None` * **Description:** Dataclass type for structured input parsing. Currently informational; used for documentation generation. ### `output_schema` * **Type:** `type | None` * **Description:** Dataclass type for structured output. Passed to agent via `ctx.output_schema`. ### `use_workspace_skills` * **Type:** `bool` * **Default:** `False` * **Description:** Whether the agent loads space skills before execution. ## Environment Configuration ### `environment` * **Type:** `dict[str, Any] | None` * **Description:** Environment variable requirements. ```python theme={null} environment={ "required": [ { "name": "API_KEY", "description": "API authentication token", "linkRef": {"provider": "anthropic", "key": "api_key"}, # Optional }, ], "optional": [ { "name": "DEBUG", "description": "Enable debug logging", }, ], } ``` Access in agent code via `ctx.env["API_KEY"]`. ## MCP Configuration ### `mcp` * **Type:** `dict[str, Any] | None` * **Description:** MCP servers to launch alongside the agent. ```python theme={null} mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "your-github-token", }, } }, "time": { "transport": { "type": "stdio", "command": "uvx", "args": ["mcp-server-time", "--local-timezone", "UTC"], } }, } ``` ## LLM Configuration ### `llm` * **Type:** `dict[str, Any] | None` * **Description:** Default LLM provider and model for the agent. ```python theme={null} llm={ "provider": "anthropic", "model": "claude-sonnet-4-6", } ``` Used when `ctx.llm.generate()` is called without explicit model. See [How to Call LLMs](/sdk/guides/call-llms) for resolution order. ## Handler Function Signature The decorated function receives: ```python theme={null} def execute(prompt: str, ctx: AgentContext) -> OkResult | ErrResult: ... ``` ### Parameters * `prompt` - The enriched prompt string from Friday (includes task, context, temporal facts) * `ctx` - [AgentContext](/sdk/python-reference/agent-context) with capabilities and metadata ### Return Types Return either: * `ok(data)` - Success with structured data * `ok(data, extras=AgentExtras(...))` - Success with metadata * `err(message)` - Failure with error message ## Example ```python theme={null} from friday_agent_sdk import agent, ok, err, AgentContext @agent( id="code-analyzer", version="1.2.0", description="Analyses code for bugs, security issues, and style violations", summary="Static analysis agent for code review", constraints="Requires read access to repository files. Does not execute code.", examples=[ "Analyse this function for security vulnerabilities", "Check this code for SQL injection risks", "Review this PR for common anti-patterns", ], environment={ "required": [ {"name": "GITHUB_TOKEN", "description": "For accessing private repos"}, ], }, mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], } } }, llm={"provider": "anthropic", "model": "claude-sonnet-4-6"}, ) def execute(prompt: str, ctx: AgentContext): # Implementation return ok({"issues": []}) ``` ## Registration Validation When you register an agent, the daemon validates metadata. Errors return: ```json theme={null} { "ok": false, "phase": "validate", "error": "description is required" } ``` ## Version Semantics Agent versions follow [Semantic Versioning](https://semver.org/): * `MAJOR` - Breaking changes to agent behavior * `MINOR` - New capabilities, backwards compatible * `PATCH` - Bug fixes, backwards compatible Friday resolves agent references to the latest semver version: ```python theme={null} # Build v1.0.0 curl -F "files=@agent.py" ... # version="1.0.0" # Build v1.0.1 curl -F "files=@agent.py" ... # version="1.0.1" # Reference in workspace.yml agents: my-agent: type: user # Resolves to v1.0.1 (latest) ``` Both versions remain on disk; rollback is possible by adjusting the space reference or rebuilding with a downgraded version. ## See Also Execution context and capabilities ok() and err() constructors Task-oriented guide for MCP tool integration # ctx.http Source: https://docs.hellofriday.ai/sdk/python-reference/http-capability HTTP capability wrapper for outbound requests through Friday's fetch layer. ## Class: Http ```python theme={null} class Http: def fetch( self, url: str, *, method: str = "GET", headers: dict[str, str] | None = None, body: str | None = None, timeout_ms: int | None = None, ) -> HttpResponse: ... ``` ## Methods ### fetch() Make an HTTP request through the host. **Parameters:** | Parameter | Type | Required | Default | Description | | ------------ | ------------------------ | -------- | ------- | ------------------------------------------------- | | `url` | `str` | Yes | - | Target URL | | `method` | `str` | No | `"GET"` | HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD) | | `headers` | `dict[str, str] \| None` | No | `None` | Request headers | | `body` | `str \| None` | No | `None` | Request body (string) | | `timeout_ms` | `int \| None` | No | `None` | Request timeout in milliseconds | **Returns:** `HttpResponse` **Raises:** `HttpError` on network-level failure (DNS, TLS, timeout, connection) ## HttpResponse ```python theme={null} @dataclass class HttpResponse: status: int # HTTP status code headers: dict[str, str] # Response headers body: str # Response body as string def json(self) -> Any: # Parse body as JSON return json.loads(self.body) ``` ## Basic GET ```python theme={null} response = ctx.http.fetch("https://api.example.com/data") if response.status == 200: data = response.json() return ok({"data": data}) elif response.status == 404: return ok({"found": False}) else: return err(f"API error: {response.status}") ``` ## POST with JSON ```python theme={null} import json response = ctx.http.fetch( "https://api.example.com/items", method="POST", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {ctx.env['API_KEY']}", }, body=json.dumps({"name": "New Item", "value": 42}), ) result = response.json() return ok({"created_id": result["id"]}) ``` ## Error Handling ```python theme={null} from friday_agent_sdk import HttpError, agent, err, ok @agent(id="resilient", version="1.0.0", description="Handles HTTP failures") def execute(prompt, ctx): try: response = ctx.http.fetch("https://api.example.com/data") except HttpError as e: # Network failure: DNS, TLS, connection, timeout return err(f"Request failed: {e}") # HTTP error status (4xx, 5xx) is NOT an exception if response.status >= 500: return err(f"Server error: {response.status}") if response.status == 429: return err("Rate limited. Please retry later.") if response.status >= 400: return err(f"Client error: {response.status} - {response.body[:200]}") return ok({"data": response.json()}) ``` ## Timeout ```python theme={null} response = ctx.http.fetch( "https://slow-api.example.com/data", timeout_ms=30000, # 30 seconds ) ``` ## Methods All HTTP methods are supported: ```python theme={null} ctx.http.fetch(url, method="GET") # Default ctx.http.fetch(url, method="POST", body="...") ctx.http.fetch(url, method="PUT", body="...") ctx.http.fetch(url, method="PATCH", body="...") ctx.http.fetch(url, method="DELETE") ctx.http.fetch(url, method="HEAD") ``` ## Headers Case-insensitive header dict: ```python theme={null} response = ctx.http.fetch( url, headers={ "Accept": "application/json", "User-Agent": "my-agent/1.0", "X-Custom-Header": "value", }, ) # Access response headers content_type = response.headers.get("content-type", "") rate_limit = response.headers.get("x-ratelimit-remaining") ``` ## Creating Artifacts Common pattern: create Friday platform artifacts via HTTP API: ```python theme={null} import json response = ctx.http.fetch( f"{ctx.config.get('platformUrl', 'http://localhost:18080')}/api/artifacts", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({ "data": { "type": "analysis", "version": 1, "data": analysis_result, }, "title": "Analysis Report", "summary": "Comprehensive analysis", }), ) if response.status < 400: artifact = response.json().get("artifact", {}) artifact_id = artifact.get("id") ... ``` ## REST API Patterns ```python theme={null} base_url = "https://api.service.com/v1" # GET collection response = ctx.http.fetch(f"{base_url}/items") items = response.json()["items"] # GET item response = ctx.http.fetch(f"{base_url}/items/{item_id}") item = response.json() # POST create response = ctx.http.fetch( f"{base_url}/items", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({"name": "New"}), ) new_item = response.json() # PUT update response = ctx.http.fetch( f"{base_url}/items/{item_id}", method="PUT", headers={"Content-Type": "application/json"}, body=json.dumps({"name": "Updated"}), ) # DELETE response = ctx.http.fetch( f"{base_url}/items/{item_id}", method="DELETE", ) ``` ## Authentication ```python theme={null} # Bearer token response = ctx.http.fetch( url, headers={"Authorization": f"Bearer {ctx.env['TOKEN']}"}, ) # Basic auth (construct manually) import base64 credentials = base64.b64encode(b"user:pass").decode() response = ctx.http.fetch( url, headers={"Authorization": f"Basic {credentials}"}, ) # API key in header response = ctx.http.fetch( url, headers={"X-API-Key": ctx.env['API_KEY']}, ) ``` ## Query Parameters Construct URL with parameters: ```python theme={null} import urllib.parse params = {"q": "search query", "limit": 10} query = urllib.parse.urlencode(params) url = f"https://api.example.com/search?{query}" response = ctx.http.fetch(url) ``` ## Response Body Limits * **5MB limit** enforced by platform * Exceeding returns truncated or error response * For large payloads, consider streaming (not yet available) ## URL Allowlists Not yet implemented. Currently all outbound HTTPS requests are allowed. ## Why Not Use `requests` or `httpx`? Agents run as native Python processes. You can `pip install` `requests` or `httpx` if needed. Host-provided HTTP is still preferred for audit logging, rate limiting, TLS management, and response limits. ## See Also Task-oriented guide # ctx.llm Source: https://docs.hellofriday.ai/sdk/python-reference/llm-capability LLM capability wrapper for routing generation requests through Friday's provider registry. ## Class: Llm ```python theme={null} class Llm: def generate( self, messages: list[dict[str, str]], *, model: str | None = None, max_tokens: int | None = None, temperature: float | None = None, provider_options: dict | None = None, ) -> LlmResponse: ... def generate_object( self, messages: list[dict[str, str]], schema: dict, *, model: str | None = None, max_tokens: int | None = None, temperature: float | None = None, provider_options: dict | None = None, ) -> LlmResponse: ... ``` ## Methods ### generate() Generate text from an LLM. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | ---------------------- | -------- | ----------------------------------------------- | | `messages` | `list[dict[str, str]]` | Yes | Conversation messages with `role` and `content` | | `model` | `str \| None` | No | Model identifier (resolution order applies) | | `max_tokens` | `int \| None` | No | Maximum tokens to generate | | `temperature` | `float \| None` | No | Sampling temperature (0.0 - 2.0) | | `provider_options` | `dict \| None` | No | Provider-specific options passthrough | **Returns:** `LlmResponse` **Raises:** `LlmError` on generation failure **Example:** ```python theme={null} result = ctx.llm.generate( messages=[{"role": "user", "content": "Summarise this article"}], model="anthropic:claude-sonnet-4-6", max_tokens=1000, temperature=0.7, ) print(result.text) ``` ### generate\_object() Generate structured output conforming to a JSON Schema. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | ---------------------- | -------- | -------------------------------- | | `messages` | `list[dict[str, str]]` | Yes | Conversation messages | | `schema` | `dict` | Yes | JSON Schema for output structure | | `model` | `str \| None` | No | Model identifier | | `max_tokens` | `int \| None` | No | Maximum tokens | | `temperature` | `float \| None` | No | Sampling temperature | | `provider_options` | `dict \| None` | No | Provider-specific options | **Returns:** `LlmResponse` with `.object` populated **Raises:** `LlmError` on generation failure **Example:** ```python theme={null} schema = { "type": "object", "properties": { "summary": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}, }, "required": ["summary"], } result = ctx.llm.generate_object( messages=[{"role": "user", "content": "Analyse this"}], schema=schema, model="anthropic:claude-haiku-4-5", ) data = result.object # Parsed JSON object print(data["summary"]) print(data.get("tags", [])) ``` ## Model Resolution Resolution order (first match wins): 1. **Fully qualified per-call** - `model="anthropic:claude-sonnet-4-6"` used directly 2. **Bare per-call + decorator default** - `model="claude-sonnet-4-6"` + `@agent(llm={"provider": "anthropic"})` resolved to full identifier 3. **Decorator default only** - `@agent(llm={"provider": "anthropic", "model": "claude-sonnet-4-6"})` used when no model specified 4. **Error** - No model specified and no decorator default ## LlmResponse ```python theme={null} @dataclass class LlmResponse: text: str | None # Generated text (None for generate_object) object: dict | None # Structured output dict (None for generate) model: str # Model identifier used (e.g., "anthropic:claude-sonnet-4-6") usage: dict # {"input_tokens": 120, "output_tokens": 250} finish_reason: str # "stop", "length", "content_filter", etc. ``` ## Error Handling ```python theme={null} from friday_agent_sdk import LlmError, agent, err, ok @agent(id="resilient", version="1.0.0", description="Handles LLM failures") def execute(prompt, ctx): try: result = ctx.llm.generate(..., model="expensive-model") except LlmError as e: # Error message from host (e.g., "Rate limit exceeded", "Invalid API key") return err(f"Primary model failed: {e}") return ok({"output": result.text}) ``` ## Provider Options Pass provider-specific configuration: ```python theme={null} result = ctx.llm.generate( messages=[...], model="anthropic:claude-sonnet-4-6", provider_options={ "anthropic": { "thinking": {"type": "enabled", "budgetTokens": 4000}, }, }, ) ``` Options vary by provider. Common patterns: **Anthropic provider:** * `thinking` - Enable extended reasoning with `{"type": "enabled", "budgetTokens": }` **Claude Code provider:** * `systemPrompt` - Either `{"type": "preset", "preset": "..."}` or `{"type": "custom", "content": "..."}` * `effort` - `"low"`, `"medium"`, `"high"` * `fallbackModel` - Model to use if primary fails * `repo` - Repository to clone and work in ## Message Format ```python theme={null} messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}, {"role": "assistant", "content": "Hi there!"}, {"role": "user", "content": "Analyse this code..."}, ] ``` Valid roles: `system`, `user`, `assistant` ## Limitations * **No streaming responses** - Full response returned at once; streaming is not yet supported * **5MB implicit limit** - Via platform constraints on response size ## Why Host-Managed? Agents run as native Python processes. You can `pip install` additional packages into the agent environment. Host-provided LLM calls are still preferred for credential management, rate limiting, provider routing, and audit logging. ## See Also Task-oriented guide Parent context object # Parse Utilities Source: https://docs.hellofriday.ai/sdk/python-reference/parse-utilities Extract structured JSON from enriched prompts sent by Friday. ## Functions ### parse\_input() Extract a JSON object from a text string, with optional dataclass validation. ```python theme={null} @overload def parse_input(prompt: str) -> dict: ... @overload def parse_input(prompt: str, schema: type[T]) -> T: ... def parse_input(prompt: str, schema: type | None = None) -> Any: ... ``` **Parameters:** | Parameter | Type | Required | Description | | --------- | -------------- | -------- | ------------------------------------ | | `prompt` | `str` | Yes | The enriched prompt text from Friday | | `schema` | `type \| None` | No | Dataclass type for typed extraction | **Returns:** `dict` or dataclass instance **Raises:** * `ValueError` - No valid JSON object found in prompt * `TypeError` - Schema is not a dataclass * `ValueError` - JSON doesn't match dataclass (missing required fields) **Example:** ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import parse_input # Plain dict extraction data = parse_input(prompt) repo = data.get("repository") branch = data.get("branch", "main") # Typed extraction @dataclass class Config: repository: str branch: str = "main" dry_run: bool = False config = parse_input(prompt, Config) # config.repository, config.branch, config.dry_run available with types ``` ### parse\_operation() Extract an operation config using a discriminator field. ```python theme={null} def parse_operation(prompt: str, schemas: dict[str, type[T]]) -> T: ... ``` **Parameters:** | Parameter | Type | Required | Description | | --------- | -------------------- | -------- | --------------------------------------- | | `prompt` | `str` | Yes | The enriched prompt text | | `schemas` | `dict[str, type[T]]` | Yes | Map of operation name to dataclass type | **Returns:** Dataclass instance for the matched operation **Raises:** `ValueError` - No valid operation config found **Example:** ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import parse_operation @dataclass class CloneConfig: operation: str # Must be "clone" repository: str branch: str = "main" @dataclass class DeployConfig: operation: str # Must be "deploy" environment: str version: str OPERATIONS = { "clone": CloneConfig, "deploy": DeployConfig, } config = parse_operation(prompt, OPERATIONS) match config.operation: case "clone": # config is typed as CloneConfig handle_clone(config) case "deploy": # config is typed as DeployConfig handle_deploy(config) ``` ## Extraction Strategy Both functions search in this order: 1. **Balanced-brace JSON objects** - Hand-rolled scanner handles arbitrary nesting 2. **Code-fenced JSON blocks** - Extracts from ` ```json ... ``` ` 3. **Full prompt** - Attempts to parse entire prompt as JSON For `parse_operation()`, only JSON objects containing an `"operation"` field are considered, and the discriminator value selects the schema. ## JSON in Markdown Input may look like: ````markdown theme={null} Task: Deploy the application Here is the configuration: ```json { "operation": "deploy", "environment": "production", "version": "1.2.3" } ``` ```` Both `parse_input()` and `parse_operation()` extract the JSON block correctly. ## Dataclass Validation When using a schema: * Only fields defined in the dataclass are extracted (unknown keys filtered) * Missing required fields raise `ValueError` with clear message * Type hints are not enforced at runtime (Python limitation) ```python theme={null} @dataclass class StrictConfig: required_field: str optional_field: int = 0 # Raises: ValueError: missing {'required_field'} config = parse_input('{"optional_field": 5}', StrictConfig) # Succeeds: required_field="value", optional_field=5 config = parse_input( '{"required_field": "value", "optional_field": 5, "extra": "ignored"}', StrictConfig, ) ``` ## Error Messages Clear errors for debugging: ```python theme={null} # No JSON found parse_input("Just text without JSON") # ValueError: No valid JSON object found in prompt. Prompt starts with: Just text... # Invalid JSON parse_input("{invalid json}") # ValueError: No valid JSON object found... # Schema mismatch parse_input('{"wrong": "fields"}', Config) # ValueError: JSON parsed but doesn't match Config: missing {'repository'} # Operation not found parse_operation('{"operation": "unknown"}', {"clone": CloneConfig}) # ValueError: No valid operation config found. Known operations: ['clone']... ``` ## Real Example: Jira Agent ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import agent, err, ok, parse_operation @dataclass class IssueViewConfig: operation: str issue_key: str @dataclass class IssueSearchConfig: operation: str jql: str max_results: int = 50 @dataclass class IssueCreateConfig: operation: str project_key: str summary: str description: str | None = None issue_type: str = "Bug" OPERATIONS = { "issue-view": IssueViewConfig, "issue-search": IssueSearchConfig, "issue-create": IssueCreateConfig, } @agent(id="jira", version="1.0.0", description="Jira operations") def execute(prompt, ctx): try: config = parse_operation(prompt, OPERATIONS) except ValueError as e: return err(str(e)) match config.operation: case "issue-view": return handle_view(config, ctx) case "issue-search": return handle_search(config, ctx) case "issue-create": return handle_create(config, ctx) case _: return err(f"Unknown operation: {config.operation}") ``` ## When to Use | Function | Use When | | ---------------------------------- | ---------------------------------------------------------- | | `parse_input(prompt)` | Single configuration, flexible parsing, no operation types | | `parse_input(prompt, Schema)` | Single configuration, want typed fields | | `parse_operation(prompt, schemas)` | Multiple operations, discriminated by `"operation"` field | ## Implementation Details The balanced-brace scanner: * Handles arbitrary nesting depth (recursive objects/arrays) * Tracks string boundaries and escape sequences * Avoids miscounting braces inside string literals * Returns all valid JSON objects found, tries each in order This hand-rolled approach is necessary because regex cannot handle arbitrary nesting depth reliably. ## See Also Task-oriented guide # Result Types Source: https://docs.hellofriday.ai/sdk/python-reference/result-types Tagged union results for agent handler returns. ## Functions ### ok() Create a success result. ```python theme={null} ok(data: object, extras: AgentExtras | None = None) -> OkResult ``` **Parameters:** | Parameter | Type | Required | Description | | --------- | --------------------- | -------- | ------------------------------------------------ | | `data` | `object` | Yes | Serializable result data (dict, list, primitive) | | `extras` | `AgentExtras \| None` | No | Optional metadata for the host | **Returns:** `OkResult` **Example:** ```python theme={null} from friday_agent_sdk import agent, ok, AgentExtras, ArtifactRef @agent(id="success", version="1.0.0", description="Returns success") def execute(prompt, ctx): # Simple result return ok({"answer": 42}) # With extras return ok( {"answer": 42}, extras=AgentExtras(reasoning="Derived from analysis"), ) # With artifact reference return ok( {"analysis_id": "123"}, extras=AgentExtras( artifact_refs=[ ArtifactRef(id="123", type="analysis", summary="Complete") ] ), ) ``` ### err() Create an error result. ```python theme={null} err(message: str) -> ErrResult ``` **Parameters:** | Parameter | Type | Required | Description | | --------- | ----- | -------- | ----------------------------------- | | `message` | `str` | Yes | Error message for the host and user | **Returns:** `ErrResult` **Example:** ```python theme={null} from friday_agent_sdk import agent, err @agent(id="checker", version="1.0.0", description="Checks prerequisites") def execute(prompt, ctx): if "API_KEY" not in ctx.env: return err("API_KEY not set. Connect the provider in Friday Link.") return ok({"status": "ready"}) ``` ## Types ### AgentResult Union type for handler return annotations: ```python theme={null} from friday_agent_sdk import AgentResult def execute(prompt, ctx) -> AgentResult: if error: return err("Something failed") return ok({"result": "success"}) ``` ### OkResult Success result dataclass: ```python theme={null} @dataclass class OkResult: data: object extras: AgentExtras | None = None ``` The `data` field serializes to JSON over NATS. Complex objects should be dicts or lists. ### ErrResult Error result dataclass: ```python theme={null} @dataclass class ErrResult: error: str ``` The `error` message is passed through to the host and displayed to the user. ### AgentExtras Optional metadata for success results: ```python theme={null} @dataclass class AgentExtras: reasoning: str | None = None artifact_refs: list[ArtifactRef] | None = None outline_refs: list[OutlineRef] | None = None ``` **Fields:** * `reasoning` - Explanation of agent decisions, shown in UI for transparency * `artifact_refs` - References to created platform artifacts * `outline_refs` - Structured entries for conversation outline ### ArtifactRef Reference to a platform artifact: ```python theme={null} @dataclass class ArtifactRef: id: str # Artifact identifier type: str # Artifact type (e.g., "analysis", "report") summary: str # Human-readable summary ``` Created via Friday's `/api/artifacts` endpoint. ### OutlineRef Structured reference for conversation outline: ```python theme={null} @dataclass class OutlineRef: service: str # Service identifier (e.g., "github", "analysis") title: str # Display title content: str | None = None # Optional content preview artifact_id: str | None = None # Linked artifact artifact_label: str | None = None # Link label text ``` ## Tagged Union Pattern `OkResult` and `ErrResult` are distinct types. It is impossible to: * Return success data with an error message * Return error data with success extras * Confuse the two in type checking ```python theme={null} from friday_agent_sdk import OkResult, ErrResult def handle(result: AgentResult): match result: case OkResult(data, extras): process_success(data, extras) case ErrResult(error): handle_error(error) ``` ## Serialisation Results serialize to a JSON envelope over NATS: ```json theme={null} { "tag": "ok" | "err", "val": "string" } ``` `AgentExtras` is serialized separately and merged by the host. ## Best Practices * **Return structured data** - Dicts with clear field names, not raw strings * **Provide reasoning** - Helps users understand agent decisions * **Create artifacts for large outputs** - Persist reports, analyses, generated code * **Use outline\_refs for scannable results** - Helps navigate complex outputs * **Handle errors early** - Validate `ctx.env`, check capabilities, return `err()` with clear messages ## Common Error Messages | Scenario | Message | | ------------------- | ------------------------------------------------------- | | Missing environment | `"{VAR} not set. Connect the provider in Friday Link."` | | Missing capability | `"{Capability} not available in this context."` | | API failure | `"{Service} API error {code}: {details}"` | | Invalid input | `"Invalid request: {reason}"` | | Timeout | `"Operation timed out after {duration}"` | ## See Also Real-time updates during execution # ctx.stream Source: https://docs.hellofriday.ai/sdk/python-reference/stream-capability Stream capability wrapper for emitting progress events and intents to the Friday UI. ## Class: StreamEmitter ```python theme={null} class StreamEmitter: def emit(self, event_type: str, data: dict | str) -> None: ... def progress(self, content: str, *, tool_name: str | None = None) -> None: ... def intent(self, content: str) -> None: ... ``` ## Methods ### emit() Emit a raw stream event to the host. **Parameters:** | Parameter | Type | Required | Description | | ------------ | ------------- | -------- | -------------------------------------------------- | | `event_type` | `str` | Yes | Event type identifier | | `data` | `dict \| str` | Yes | Event payload (dict serialized to JSON, or string) | **Example:** ```python theme={null} ctx.stream.emit("custom-phase", {"step": 3, "total": 10}) ctx.stream.emit("debug", "Processing complete") ``` ### progress() Emit a `data-tool-progress` event for UI progress display. **Parameters:** | Parameter | Type | Required | Default | Description | | ----------- | ------------- | -------- | ------- | ---------------------------- | | `content` | `str` | Yes | - | Progress message | | `tool_name` | `str \| None` | No | `None` | Tool identifier for grouping | **Example:** ```python theme={null} ctx.stream.progress("Starting analysis...") ctx.stream.progress("Fetching repository data", tool_name="GitHub") ctx.stream.progress("Analysing code patterns", tool_name="Analyser") ``` ### intent() Emit a `data-intent` event for high-level state changes. **Parameters:** | Parameter | Type | Required | Description | | --------- | ----- | -------- | ------------------ | | `content` | `str` | Yes | Intent description | **Example:** ```python theme={null} ctx.stream.intent("Discovering repository structure") ctx.stream.intent("Identifying security issues") ctx.stream.intent("Generating recommendations") ``` ## Common Usage Patterns ### Phase-Based Progress ```python theme={null} def execute(prompt, ctx): ctx.stream.progress("Phase 1: Parsing input") config = parse_input(prompt) ctx.stream.progress("Phase 2: Fetching data", tool_name="GitHub") data = ctx.tools.call("fetch_repo", config) ctx.stream.progress("Phase 3: Analysing", tool_name="LLM") analysis = ctx.llm.generate(...) ctx.stream.progress("Phase 4: Finalising") return ok({"result": analysis.text}) ``` ### Intent for State Changes ```python theme={null} def execute(prompt, ctx): ctx.stream.intent("Understanding task requirements") requirements = extract_requirements(prompt) ctx.stream.intent("Planning approach") plan = create_plan(requirements) ctx.stream.intent("Executing plan") for step in plan.steps: ctx.stream.progress(f"Step {step.number}: {step.description}") execute_step(step) ctx.stream.intent("Finalising results") return ok({"completed": True}) ``` ### Tool-Associated Progress ```python theme={null} def execute(prompt, ctx): ctx.stream.progress("Initialising", tool_name="Setup") ctx.stream.progress("Querying database", tool_name="PostgreSQL") rows = ctx.tools.call("query", {"sql": "SELECT ..."}) ctx.stream.progress("Processing results", tool_name="Processor") processed = [transform(r) for r in rows] ctx.stream.progress("Storing analysis", tool_name="Storage") ctx.http.fetch(..., method="POST", body=json.dumps(processed)) ctx.stream.progress("Complete", tool_name="Setup") return ok({"count": len(processed)}) ``` ### Fallback When Unavailable `ctx.stream` is always present. It is a safe no-op in test contexts: ```python theme={null} def execute(prompt, ctx): # Safe wrapper def progress(msg, tool=None): if ctx.stream: ctx.stream.progress(msg, tool_name=tool) progress("Starting...") # Work... progress("Complete") return ok({"done": True}) ``` ## Emission During LLM Calls Progress emits are fire-and-forget over NATS - they do not block: ```python theme={null} ctx.stream.progress("Starting LLM call...") # Sent immediately # LLM call blocks until response; progress already sent result = ctx.llm.generate(messages, model="claude-sonnet-4-6") # Back in Python ctx.stream.progress("LLM complete") # Sent now ``` The host may emit its own progress events during the suspension. ## Event Types Standard types used by Friday: | Type | Usage | | -------------------- | ----------------------------------------- | | `data-tool-progress` | Agent progress updates (use `progress()`) | | `data-intent` | High-level state changes (use `intent()`) | | `data-error` | Error events (usually emitted by host) | Custom types can be emitted via `emit()` but may not have UI handlers. ## Best Practices * **Emit before expensive operations** - Warn users before long LLM calls * **Use tool\_name for grouping** - Helps UI organise progress by component * **Keep messages concise** - 50-100 characters ideal for UI display * **Avoid tight loop emission** - Batch or debounce high-frequency updates * **Prefer intent for phases, progress for detail** - Two-level hierarchy * **Always safe to call** - `ctx.stream` never None, but may no-op in tests ## When to Emit | Scenario | Method | Example | | ------------------ | ------------------------- | ---------------------------------- | | Starting a phase | `intent()` | "Analysing repository" | | Detailed progress | `progress()` | "Fetching 50 files..." | | Tool-specific work | `progress(tool_name=...)` | tool\_name="GitHub" | | Fallback scenarios | `progress()` | "Retrying with alternate model..." | | Completion | `intent()` | "Analysis complete" | ## See Also Task-oriented guide The subprocess model and host capabilities # ctx.tools Source: https://docs.hellofriday.ai/sdk/python-reference/tools-capability MCP tool capability wrapper for invoking Model Context Protocol servers. ## Class: Tools ```python theme={null} class Tools: def list(self) -> list[ToolDefinition]: ... def call(self, name: str, args: dict) -> dict: ... ``` ## Methods ### list() List all available tools from configured MCP servers. **Returns:** `list[ToolDefinition]` **Example:** ```python theme={null} tools = ctx.tools.list() for tool in tools: print(f"{tool.name}: {tool.description}") print(f" Schema: {tool.input_schema}") ``` ### call() Call a tool by name with arguments. **Parameters:** | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------- | | `name` | `str` | Yes | Tool identifier | | `args` | `dict` | Yes | Arguments matching tool's input schema | **Returns:** `dict` - Tool output parsed from JSON **Raises:** `ToolCallError` on tool execution failure **Example:** ```python theme={null} # Call with typed arguments result = ctx.tools.call( "search_issues", { "query": "is:open label:bug", "repo": "my-org/my-repo", }, ) # Access result fields issues = result["issues"] count = result["count"] ``` ## ToolDefinition ```python theme={null} @dataclass class ToolDefinition: name: str # Tool identifier (unique within server) description: str # Human-readable description input_schema: dict # JSON Schema for arguments ``` ## Configuration MCP servers are configured in the `@agent` decorator: ```python theme={null} @agent( id="github-helper", version="1.0.0", description="Uses GitHub", mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "your-github-token", }, } } }, ) def execute(prompt, ctx): # ctx.tools.list() includes tools from github server result = ctx.tools.call("search_issues", {...}) ... ``` ## Error Handling ```python theme={null} from friday_agent_sdk import ToolCallError, agent, err, ok @agent(id="safe-caller", version="1.0.0", description="Handles tool errors") def execute(prompt, ctx): try: result = ctx.tools.call("risky_operation", {"data": prompt}) except ToolCallError as e: return err(f"Tool failed: {e}") return ok({"result": result}) ``` ## Finding Tools Filter by name pattern: ```python theme={null} tools = ctx.tools.list() search_tools = [t for t in tools if "search" in t.name] git_tools = [t for t in tools if t.name.startswith("git")] ``` ## Dynamic Tool Selection ```python theme={null} def execute(prompt, ctx): tools = ctx.tools.list() # Find appropriate tool based on prompt if "issue" in prompt.lower(): tool = next((t for t in tools if "issue" in t.name), None) elif "pr" in prompt.lower() or "pull" in prompt.lower(): tool = next((t for t in tools if "pull" in t.name), None) else: return err("No appropriate tool found") if not tool: available = [t.name for t in tools] return err(f"Tool not found. Available: {available}") result = ctx.tools.call(tool.name, {"query": prompt}) return ok({"result": result}) ``` ## Real Example: Time Operations ```python theme={null} @agent( id="time-agent", version="1.0.0", description="Time conversion", mcp={ "time": { "transport": { "type": "stdio", "command": "uvx", "args": ["mcp-server-time", "--local-timezone", "UTC"], } } }, ) def execute(prompt, ctx): # Discover available tools tools = ctx.tools.list() tool_names = [t.name for t in tools] # Call current time now = ctx.tools.call("get_current_time", {"timezone": "UTC"}) # Convert time converted = ctx.tools.call( "convert_time", { "source_timezone": "UTC", "time": "14:30", "target_timezone": "America/New_York", }, ) return ok({ "current_utc": now, "converted": converted, "available_tools": tool_names, }) ``` ## Multiple MCP Servers Tools from all configured servers are merged into a single namespace: ```python theme={null} @agent( id="multi", version="1.0.0", mcp={ "github": {...}, "postgres": {...}, }, ) def execute(prompt, ctx): all_tools = ctx.tools.list() # Contains tools from both github and postgres servers github_count = len([t for t in all_tools if "github" in t.name]) db_count = len([t for t in all_tools if "sql" in t.name]) ... ``` ## Tool Chaining ```python theme={null} def execute(prompt, ctx): # Step 1: Search search_result = ctx.tools.call( "search_issues", {"query": prompt}, ) # Step 2: Get details for top result top_issue = search_result["issues"][0] details = ctx.tools.call( "get_issue", { "owner": "my-org", "repo": "my-repo", "issue_number": top_issue["number"], }, ) # Step 3: Add comment ctx.tools.call( "add_issue_comment", { "owner": "my-org", "repo": "my-repo", "issue_number": top_issue["number"], "body": "Analyzing this issue now...", }, ) return ok({"analyzed": top_issue["title"]}) ``` ## Schema Inspection ```python theme={null} tool = next(t for t in ctx.tools.list() if t.name == "create_issue") # Inspect required fields schema = tool.input_schema required = schema.get("required", []) properties = schema.get("properties", {}) for field in required: print(f"Required: {field} ({properties[field].get('type')})") ``` ## Common MCP Servers | Server | Package | Tools | | ---------- | ----------------------------------------- | ------------------------------------------------------------- | | GitHub | `@modelcontextprotocol/server-github` | search\_issues, get\_issue, create\_issue, add\_comment, etc. | | PostgreSQL | `@modelcontextprotocol/server-postgres` | query, list\_tables, describe\_table | | Time | `mcp-server-time` | get\_current\_time, convert\_time | | Filesystem | `@modelcontextprotocol/server-filesystem` | read\_file, write\_file, list\_directory | | Fetch | `@modelcontextprotocol/server-fetch` | fetch (HTTP requests) | ## Transport Types Currently supported: `stdio` Planned: `sse` (Server-Sent Events) ## Environment Variables Pass environment variables directly to MCP server processes: ```python theme={null} mcp={ "github": { "transport": { "type": "stdio", "command": "npx", "args": ["..."], "env": { "GITHUB_TOKEN": "your-github-token", } } } } ``` To declare environment variables your agent code accesses via `ctx.env`, use the `environment` decorator field: ```python theme={null} @agent( ..., environment={ "required": [ {"name": "GITHUB_TOKEN", "description": "GitHub API token"}, ] }, ) ``` ## See Also Task-oriented guide Official protocol documentation Community server directory # Quickstart Source: https://docs.hellofriday.ai/sdk/quickstart Build a text analysis agent from an empty directory to a running result. Build a text analysis agent that accepts a topic and returns a structured analysis with a summary, key points, and a sentiment rating. It demonstrates: * The `@agent` decorator for metadata * Calling an LLM through `ctx.llm.generate_object()` for structured output * Returning structured data with `ok()` ## Prerequisites * Friday Studio installed and running - daemon reachable at `http://localhost:18080` * Python 3.11+ and [`uv`](https://docs.astral.sh/uv/) (for IDE support) * A text editor (VS Code recommended) ## Step 1: Set up IDE support You need the SDK installed locally for autocomplete and type checking. Create a Python environment and install the SDK: ```bash theme={null} # 1. Clone the SDK somewhere (one-time) git clone git@github.com:friday-platform/agent-sdk.git ~/agent-sdk # 2. Create a venv in your agent project directory mkdir -p ~/my-agents && cd ~/my-agents uv venv source .venv/bin/activate uv pip install -e ~/agent-sdk/packages/python ``` Create `.vscode/settings.json` in your agent directory: ```json theme={null} { "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.analysis.typeCheckingMode": "basic" } ``` Reload VS Code after creating the file. Cmd+click on `AgentContext` in the next step should jump to the SDK definition. ## Step 2: Create the agent file Create a directory for your agent anywhere on your machine: ```bash theme={null} mkdir -p ~/my-agents/text-analyzer ``` This agent accepts text input and returns structured analysis. It uses: * A `@dataclass` to define the output shape * `ctx.llm.generate_object()` to request structured JSON from an LLM * The host's LLM provider - your code never handles API keys Write `~/my-agents/text-analyzer/agent.py`: ```python theme={null} from dataclasses import dataclass from friday_agent_sdk import agent, ok, AgentContext, run @dataclass class AnalysisResult: summary: str key_points: list[str] sentiment: str # "positive", "negative", or "neutral" @agent( id="text-analyzer", version="1.0.0", description="Analyzes text and returns structured summary, key points, and sentiment", ) def execute(prompt: str, ctx: AgentContext): """Analyze the user's text using an LLM.""" # Define the schema for structured output output_schema = { "type": "object", "properties": { "summary": {"type": "string"}, "key_points": { "type": "array", "items": {"type": "string"}, }, "sentiment": { "type": "string", "enum": ["positive", "negative", "neutral"], }, }, "required": ["summary", "key_points", "sentiment"], "additionalProperties": False, } # Call Friday's LLM provider - your agent never sees the API key analysis_prompt = f"""Analyze the following text. Text: {prompt} Provide a concise summary, 3-5 key points, and an overall sentiment.""" result = ctx.llm.generate_object( messages=[{"role": "user", "content": analysis_prompt}], schema=output_schema, model="anthropic:claude-haiku-4-5", # Fast and cost-effective ) # result.object contains the parsed JSON matching our schema return ok(result.object) if __name__ == "__main__": run() ``` ## Step 3: Build and test Register your agent with Friday: ```bash theme={null} friday agent register ~/my-agents/text-analyzer ``` The daemon validates the agent, copies source files, and hot-reloads the registry. No restart required. Verify the registration succeeded: ```bash theme={null} friday agent list | grep text-analyzer # text-analyzer@1.0.0 ``` Test your agent with the CLI (requires Studio running at `http://localhost:15200`): ```bash theme={null} friday agent exec text-analyzer \ -i "The new feature shipped on time and customers report faster load times. Support tickets are down 40%." ``` Or hit the daemon directly: ```bash theme={null} curl -s -X POST http://localhost:18080/api/agents/text-analyzer/run \ -H 'Content-Type: application/json' \ -d '{"input": "The new feature shipped on time and customers report faster load times. Support tickets are down 40%."}' | jq . ``` The response streams as SSE events. After a moment you see the result: ```json theme={null} { "summary": "Product launch successful with measurable performance improvements", "key_points": [ "Feature shipped on schedule", "Load times significantly improved", "Support tickets decreased by 40%" ], "sentiment": "positive" } ``` Try a different input: ```bash theme={null} friday agent exec text-analyzer \ -i "The server crashed twice today. The database is throwing connection errors and the logs are incomprehensible." ``` Your agent classifies this as `"sentiment": "negative"`. Add `--json` for raw NDJSON output, useful for piping to `jq`: ```bash theme={null} friday agent exec text-analyzer -i "analyze this text" --json | jq . ``` ## Step 4: Iterate Edit `~/my-agents/text-analyzer/agent.py`, then re-register and test: ```bash theme={null} friday agent register ~/my-agents/text-analyzer friday agent exec text-analyzer -i "test your changes" ``` This cycle - edit, restart, test - is your development loop. **There's a skill for that.** The [`writing-friday-python-agents`](https://github.com/friday-platform/agent-sdk/tree/main/packages/python/skills/writing-friday-python-agents) skill lets coding agents like Claude Code write and modify Friday agents directly - correct imports and proper capability calls. Bump the version to keep old builds available for rollback: ```python theme={null} @agent( id="text-analyzer", version="1.0.1", # Bumped from 1.0.0 description="Analyzes text with an LLM", ) ``` Both versions are stored, but Friday resolves `text-analyzer` to the latest semver version (`1.0.1`). ## Step 5: Register in a space (optional) To use your agent within a Friday space (for planner routing, signals, and multi-agent orchestration), add it to your space's `workspace.yml`: ```yaml workspace.yml theme={null} agents: text-analyzer: type: user ``` The `type: user` field tells Friday this is a custom Python agent. The `id` must match the `id` in the `@agent` decorator. This step is not required for direct execution. ## Advanced topics For CI/CD pipelines or automation, register agents via the daemon API on port `18080`: ```bash theme={null} curl -X POST http://localhost:18080/api/agents/register \ -H "Content-Type: application/json" \ -d '{"entrypoint": "/abs/path/to/agent.py"}' \ | jq . ``` Error responses include the phase that failed (`prereqs`, `validate`, or `write`): ```json theme={null} { "ok": false, "phase": "validate", "error": "description is required" } ``` **Agent not found after registering** - Check that registration returned `{"ok": true, ...}`. Verify the agent ID matches what you pass to the execute command. Run `friday agent list` to see all registered agents. **Build fails with syntax errors** - The SDK uses pure Python dataclasses - no Pydantic. Ensure your type hints use standard library types only. **Registration returns 400** - Your `@agent` decorator metadata failed validation. Required fields: `id`, `version`, `description`. **ImportError on third-party packages** - Ensure the package is installed in the agent's Python environment. You can `pip install` additional pure-Python packages. Use `ctx.http` and `ctx.llm` for I/O that needs host audit logging and credential management. **Credentials not working** - Verify your `.env` file contains `ANTHROPIC_API_KEY` and re-register the agent: `friday agent register /abs/path/to/agent.py`. ## Next steps Different models, structured output, and error handling. Fetch data from external APIs. Invoke GitHub, databases, and other MCP servers. The subprocess model and host capabilities. # Security Source: https://docs.hellofriday.ai/security How Friday Studio handles your data, credentials, and vulnerabilities. Friday Studio runs locally on your machine. Your data — spaces, session history, artifacts, and credentials — stays on your machine and is never sent to Friday's servers. Everything Friday stores lives in the Friday home directory (default `~/.friday/local/`) on your machine — spaces, sessions, artifacts, and credentials. Friday does not have access to any of it. API keys and OAuth tokens are stored locally by the Link service and injected into agent processes at runtime. They are never written into `workspace.yml` or logged. All Friday services bind to `127.0.0.1` and are not accessible from other machines on your network. Inbound webhook payloads are forwarded from Cloudflare's edge to your local daemon. Disable entirely with `NO_TUNNEL=true`. Friday's open-code codebase is scanned continuously for dependency vulnerabilities, with automated checks on every code change. ## Reporting a vulnerability Do not open a public GitHub issue for security problems. Use one of these private channels: * **GitHub private advisory** — [open a confidential report](https://github.com/friday-platform/friday-studio/security/advisories/new), visible only to maintainers * **Email** — [security@hellofriday.ai](mailto:security@hellofriday.ai) Include a description of the issue, affected component, steps to reproduce, and any relevant logs or proof-of-concept. We'll acknowledge within 3 business days and provide an initial assessment within 7. Coordinated disclosure defaults to 90 days from the initial report. ## Supported versions Security fixes are applied to the `main` branch and shipped in the next release. We do not backport to older releases — run a recent build. ## Contact For security questions: [security@hellofriday.ai](mailto:security@hellofriday.ai)