# NoClick > NoClick is an assistant you text. Message it on WhatsApp or iMessage, email it, call it or use the web chat, and it builds and runs what you ask for: websites and apps, AI agents that work in your connected accounts, automations, phone numbers and calls, domains and inboxes. It asks before it buys anything. Developers get the same pieces as an API, with Python and TypeScript SDKs, a CLI and an MCP server. Every service costs what its provider charges NoClick; the plan's only margin is a fee when you add funds. NoClick has two sides, on one account and one balance: - **The assistant, for anyone.** Text NoClick on [WhatsApp](https://wa.me/14242421064) (+14242421064) or iMessage (+16126493126): the first text makes an account, with nothing to sign up for. After that you can also call it, email it at the address it gives you, or chat at https://www.noclick.com/dashboard. It builds and publishes websites, buys a domain or a phone number once you approve the price, places calls for you and sends back the recording, builds agents and automations that run on schedules and app events in the accounts you connect (144+ apps, from Gmail and Slack to Stripe), reports back, reminds you, and looks into runs that fail. - **The platform, for developers and their coding agents.** The same pieces through the API at https://api.noclick.com (every path under `/v1`, authenticated with a bearer `nk_live_` key), the SDKs (`pip install noclick`, `npm install noclick`), the `noclick` CLI and the MCP server at https://mcp.noclick.com. Run hosted agents for your own users, in their connected accounts, with phone numbers, inboxes, domains, apps, databases and storage, and see every operation and dollar per user. Start with the [Introduction](https://www.noclick.com/docs/guide/intro) and [Getting started](https://www.noclick.com/docs/guide/getting-started). **Pricing.** Every service (AI models, phone numbers and calls, domains, email, sandboxes and the rest) costs exactly what its provider charges NoClick, through the API too. The one margin is a fee added when you add funds to your balance: 15% on Free ($0, with $1 of usage included a day, up to $5 a month) and 5% on Member ($25 a month, with $10 of usage included and no NoClick branding). Work run on your own model key or ChatGPT or Claude subscription carries no fee. Buying a phone number or a domain needs a $5 balance from funds you added. Prices for each service: https://www.noclick.com/pricing. **Give a coding agent a key without showing it the key.** In the [developer console](https://www.noclick.com/dashboard/developers), Quickstart's prompt carries a one-time setup code (good for 30 minutes), and the agent's one command writes a new key straight into the project's `.env` ([API keys and scopes](https://www.noclick.com/docs/guide/keys)): ```bash curl -fsS -X POST https://api.noclick.com/v1/setup -d code=nc_setup_... >> .env ``` **Connect an MCP client** (Claude, ChatGPT, Cursor, Codex) to `https://mcp.noclick.com` over Streamable HTTP, signing in with OAuth or sending `Authorization: Bearer nk_live_...`. In Claude Code: `claude mcp add --transport http noclick https://mcp.noclick.com`, then `/mcp` to sign in ([The NoClick MCP server](https://www.noclick.com/docs/guide/mcp)). Anything marked invite-only beta answers 404 or `not_available` until NoClick grants access (ask support@noclick.com). ## Guide: Get started - [Introduction](https://www.noclick.com/docs/guide/intro): Everything your AI product runs on, through one SDK and priced at cost: agents, your users' accounts, phone numbers, inboxes, domains, apps, databases and storage. - [Getting started](https://www.noclick.com/docs/guide/getting-started): Create a key, install the SDK and make your first calls. ## Guide: Your users - [Users](https://www.noclick.com/docs/guide/users): A user is someone your product acts for. Everything you do for them is scoped to them. - [Sessions for the browser](https://www.noclick.com/docs/guide/sessions): Give your frontend a short-lived token that acts as one user, so your secret key never leaves your server. - [Brand](https://www.noclick.com/docs/guide/brand): Show your product's name, logo and colour on the pages and emails NoClick sends your users. ## Guide: Their accounts - [Connections](https://www.noclick.com/docs/guide/connections): Name each account your code and agents act in, let your users connect it once, and use it by name. - [Calling integrations](https://www.noclick.com/docs/guide/integrations): Find a service, see what it can do, and run its operations, including the ones that need no account and no connection. ## Guide: Hosted agents - [Agents](https://www.noclick.com/docs/guide/agents): Create a hosted agent in one call, give it your users' connections, and message it. - [Triggers and automations](https://www.noclick.com/docs/guide/triggers): Wake an agent on a webhook, an app event, one of NoClick's own events, a schedule (as an automation) or each message on a queue. - [Memory and files](https://www.noclick.com/docs/guide/memory): Give an agent a memory it keeps across conversations, share memories between agents, and give it files it works on in each one. - [Sharing and streaming](https://www.noclick.com/docs/guide/sharing): Share an agent at a public link, stream its turn live into your own UI, and follow a conversation as it moves. - [Test runs](https://www.noclick.com/docs/guide/testing): Rehearse an agent against a simulated world: real turns, no side effects. - [Letting agents ask for connections](https://www.noclick.com/docs/guide/grants): Let an agent ask its user to connect the accounts it turns out to need, within limits your code sets. - [Coordinators and sub-agents](https://www.noclick.com/docs/guide/sub-agents): Let one agent spawn others, hear from them when it must, ask them anything without stopping them, and let agents outside NoClick talk to yours. - [Channels](https://www.noclick.com/docs/guide/channels): Put agents, your user and agents from elsewhere in one room, with hard stops on turns and spend. - [Workspaces](https://www.noclick.com/docs/guide/workspaces): Group channels, agents and your users into workspaces, each with its own members, subscriptions and monthly spend cap. ## Guide: Tools for your own LLM - [Toolkits](https://www.noclick.com/docs/guide/toolkits): Give your own LLM calls tools that run in your users' connected accounts, plus web search, page fetching and a code sandbox. - [Hosted MCP servers](https://www.noclick.com/docs/guide/mcp-servers): Put your tools at one URL for Claude, ChatGPT, Cursor or any MCP client. - [AI models](https://www.noclick.com/docs/guide/ai): Chat, embeddings, images, videos and transcription paid from your NoClick balance, an OpenAI-compatible endpoint, and your agents as models. - [The NoClick MCP server](https://www.noclick.com/docs/guide/mcp): Let Claude, ChatGPT, Cursor or any MCP client build on the platform, signed in with OAuth or with a platform key. - [The workflow MCP server](https://www.noclick.com/docs/guide/workflow-mcp): Edit a workflow node by node from an MCP client connected to the app's own server, or from an agent inside a workflow with the NoClick node. ## Guide: People in the loop - [Asking people](https://www.noclick.com/docs/guide/humans): Ask a person a question, get their approval or send them a notice, then wait for the answer. - [Policy and approvals](https://www.noclick.com/docs/guide/policy): Decide which tools may run, which never may, and which need a person to say yes first. ## Guide: Acting in the world - [Inboxes](https://www.noclick.com/docs/guide/inboxes): Give your code or an agent its own email address, read what arrives, reply, and write new mail from your own domain. - [Phone numbers](https://www.noclick.com/docs/guide/phone-numbers): Buy a real phone number or bring your own, have an agent answer its calls, and have it call people with a goal. - [Browsers](https://www.noclick.com/docs/guide/browser): Give an agent a browser, and keep a person's logins in a profile it can use. - [Cards](https://www.noclick.com/docs/guide/cards): Virtual cards with limits and approvals, for agents that buy things. (invite-only beta) - [Sandboxes](https://www.noclick.com/docs/guide/sandboxes): Start an isolated Linux machine, run commands and move files in and out, billed by the second. - [Machines](https://www.noclick.com/docs/guide/machines): Run agents on an always-on machine at a flat monthly price, instead of a sandbox per conversation. ## Guide: Build and host - [Apps and deployments](https://www.noclick.com/docs/guide/apps): Deploy a repo to a URL, with previews, bindings, secrets, schedules, logs and traffic. - [Code repos](https://www.noclick.com/docs/guide/repos): Git repositories your code and agents commit to, with plain git access and export to GitHub. - [Email, payments and sign-in](https://www.noclick.com/docs/guide/app-services): Transactional email, payments and sign-in for an app, without accounts of your own. - [Domains](https://www.noclick.com/docs/guide/domains): Buy a domain in your user's name, connect one they own or transfer it in, and point it at an app or an inbox. - [Workflows](https://www.noclick.com/docs/guide/workflows): Build workflows from a description, read them in full, run, pause and publish them, or give them an endpoint. ## Guide: Data - [Databases](https://www.noclick.com/docs/guide/databases): A SQL database for an app or an agent. SQLite by default, Postgres when you outgrow it, with migrations, restores and branches. - [Storage](https://www.noclick.com/docs/guide/storage): Buckets of files by key, private by default, with signed links, public URLs and S3-compatible access. - [Volumes](https://www.noclick.com/docs/guide/volumes): A persistent filesystem you mount into sandboxes and agents, for datasets, caches and build output. - [Dicts](https://www.noclick.com/docs/guide/dicts): A key-value store for small shared state, what has been seen, counters, locks and caches with expiry, that reads like a Python dict. - [Queues](https://www.noclick.com/docs/guide/queues): Durable background work between your code, workers and agents, delivered at least once, with retries and dead letters. - [Secrets](https://www.noclick.com/docs/guide/secrets): Named environment values, like API keys, for the code your sandboxes, agents and apps run. ## Guide: In production - [Operations](https://www.noclick.com/docs/guide/operations): Anything that waits on the world returns an operation. Block on it, poll it, or come back to it by id. - [Undo and the trash](https://www.noclick.com/docs/guide/undo): Deleting an app or a resource moves it to the trash for 30 days, a reversible write records its undo, and a dict, a secret or a setting keeps what it held. Only purging is permanent. - [Activity](https://www.noclick.com/docs/guide/activity): Read back everything done with your account, from operations and tool calls to API requests, across every user. - [Webhooks and events](https://www.noclick.com/docs/guide/webhooks): Get every event that matters delivered to your server, signed, with retries. - [Spend and limits](https://www.noclick.com/docs/guide/spend): See what your account spends per user, key and agent, cap each of them, and charge your users for theirs. - [Regions](https://www.noclick.com/docs/guide/regions): Choose where sandboxes and agents run, set your account's default, and keep resources together. - [Errors and retries](https://www.noclick.com/docs/guide/errors): Every failure is a typed error that says what to do next. Transient ones are retried for you, safely. - [Export](https://www.noclick.com/docs/guide/export): Pack up everything a user or an app holds, in formats that work elsewhere. ## Guide: SDK and API - [API keys and scopes](https://www.noclick.com/docs/guide/keys): Create keys with only the access they need, pin them to a user, and cap what they spend. - [Access for AI agents](https://www.noclick.com/docs/guide/agent-access): How an AI agent gets a NoClick key, by signing itself up or from a person, and what it can do before a person claims its account. - [Async and concurrency](https://www.noclick.com/docs/guide/async): Every method is also awaitable, and the user you act for follows your code across tasks and threads. - [The HTTP API](https://www.noclick.com/docs/guide/http-api): Call the platform from any language. The SDKs are thin clients over these conventions. - [The CLI](https://www.noclick.com/docs/guide/cli): Sign in, chat with agents, query databases, copy files, deploy apps, buy domains and forward webhooks from your terminal. ## Examples - [All examples](https://www.noclick.com/docs/examples): Complete programs you can run. Each one is also a guide: read it top to bottom, then run it. - [An assistant for every user, in their own Gmail and Calendar](https://www.noclick.com/docs/examples/assistant-for-every-user): A web app where each of your users connects their own Gmail and Google Calendar and gets an assistant that works only in their accounts, under a monthly spending cap. - [One support agent for chat, email and phone](https://www.noclick.com/docs/examples/support-agent): One support agent on your site's chat, your support email address and a phone number, answering from your help center, looking up the customer's own orders in Stripe, and asking your team before large refunds. - [An accounts-payable clerk with its own inbox](https://www.noclick.com/docs/examples/accounts-payable-inbox): An accounts-payable clerk with its own email address that checks each vendor invoice against QuickBooks, gets a person's approval over a limit, pays with a single-use card and replies to the vendor. (uses invite-only beta APIs) - [An AI receptionist on a real phone number](https://www.noclick.com/docs/examples/phone-receptionist): A real phone number that answers like a front desk, books into the business's Google Calendar, texts callers their confirmation and puts the hard calls through to the owner. (uses invite-only beta APIs) - [Turn labelled GitHub issues into pull requests](https://www.noclick.com/docs/examples/issue-to-pull-request): Label a GitHub issue `agent` and Claude Code reproduces the bug, fixes it, runs your tests and opens a pull request. - [Describe an app, get a live URL](https://www.noclick.com/docs/examples/prompt-to-deployed-app): Your users describe an app, a Codex agent builds it, and it goes live on your domain with the database, email and payments it asks for. - [Ask your sales calls anything](https://www.noclick.com/docs/examples/sales-call-search): Turn a folder of sales-call recordings into something you can ask, answered with quotes, timestamps and a link to the moment. ## Reference and changelog - [Python SDK reference](https://www.noclick.com/docs/reference/python): Every class, method and error in the noclick package (pip install noclick). - [TypeScript SDK reference](https://www.noclick.com/docs/reference/typescript): The same reference in TypeScript (npm install noclick); Node 20+, Deno, Bun and browsers. - [Changelog](https://www.noclick.com/docs/changelog): What changed in NoClick: the SDK, the API and the app, newest first. - [Changelog feed](https://www.noclick.com/docs/changelog/feed.xml): The changelog as an Atom feed. ## Product - [Pricing](https://www.noclick.com/pricing): The two plans, Free and Member, and every service's price. - [Integrations](https://www.noclick.com/integrations): The 144+ apps NoClick connects to, each with its operations. - [Agents](https://www.noclick.com/agents): Agents on Claude Code, Codex, OpenCode, OpenClaw or Hermes, with your apps as tools. - [Templates](https://www.noclick.com/templates): Ready-to-run agents for sales, support, marketing and ops. - [MCP servers from your apps](https://www.noclick.com/mcp): Turn the apps you connect into an MCP server for Claude, Cursor or any MCP client. - [Support](https://www.noclick.com/support): Email support@noclick.com, the docs, service status, privacy requests and security reports. ## Optional - [Full text](https://www.noclick.com/llms-full.txt): This index followed by every guide page and example in full, as Markdown. - [Developer Terms](https://www.noclick.com/legal/developer-terms): NoClick's Developer Terms: the agreement for building on the NoClick Platform, its API, SDKs, MCP server and developer console. - [Acceptable Use Policy](https://www.noclick.com/legal/acceptable-use): What you, your users and your agents may not do with the NoClick Platform. - [Domain Registration Agreement](https://www.noclick.com/legal/domain-registration): The agreement for a domain name registered through NoClick, which resells registration from Name.com. - [Subprocessors](https://www.noclick.com/legal/subprocessors): The companies NoClick uses to run its service and process customer data, and what each one does. - [Privacy Policy](https://www.noclick.com/privacy-policy) - [Terms of Service](https://www.noclick.com/terms-of-service) --- # Introduction Source: https://www.noclick.com/docs/guide/intro Everything your AI product runs on, through one SDK and priced at cost: agents, your users' accounts, phone numbers, inboxes, domains, apps, databases and storage. With the NoClick Platform you can: - Run **hosted agents** on NoClick's own loop or a coding agent like Claude Code or Codex, answering your users by message, email and phone, with the tools you choose - Let your users **connect their own accounts** (Gmail, Slack, Stripe and more) via a NoClick-generated link you can share with them - Give agents what they act with: **phone numbers, inboxes, browsers and domains** - **Build and host** apps on your domains, from code repos, with sandboxes and machines to run code in - Keep **data** in databases, storage, volumes, queues and secrets - See every **operation, tool call, request and dollar**, per user, with spend caps and policy There's nothing to configure outside your code. Your users, their connections, your agents and the rules they follow are all API calls. Here's a complete example: an assistant that works with one of your users' own Gmail. ```python import noclick # One of your users, by your id for them, created the first time it's used user = noclick.User.from_id("user_42", create_if_missing=True, email="ana@example.com") with noclick.as_user(user): # Ana's Gmail, under a name your code uses. Pending until she connects it. gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True) # Ana's assistant, acting in exactly that account assistant = noclick.Agent.from_name( "assistant", create_if_missing=True, instructions="You help the user with their email. Be brief.", tools=[gmail], ) reply = assistant.message("What did my accountant send this week?") print(reply.text) ``` ```ts import { Agent, Connection, User, asUser } from "noclick"; // One of your users, by your id for them, created the first time it's used const user = User.fromId("user_42", { createIfMissing: true, email: "ana@example.com" }); await asUser(user, async () => { // Ana's Gmail, under a name your code uses. Pending until she connects it. const gmail = Connection.fromName("gmail", { integration: "gmail", createIfMissing: true }); // Ana's assistant, acting in exactly that account const assistant = Agent.fromName("assistant", { createIfMissing: true, instructions: "You help the user with their email. Be brief.", tools: [gmail], }); const reply = await assistant.message("What did my accountant send this week?"); console.log(reply.text); }); ``` `gmail` is a named **connection**: one account, which the agent always acts in. Ana hasn't connected it yet, so the agent's Gmail tools answer with the connection's link and the agent passes it on in its reply (your code can also show `gmail.connect_url` itself). Once she connects, the same call reads her inbox. Run the same code for any other user and they get their own connection and their own assistant. ## How does it work? NoClick runs the agents, stores your users' connected accounts encrypted, keeps OAuth tokens fresh, and executes each integration operation. Your code holds a secret API key and calls the API. You never store a user's token or run an agent loop yourself. Anything that waits on the world returns an [operation](https://www.noclick.com/docs/guide/operations): a connect link waits for the user, an approval for a person, an agent turn for the model. You can block on it with `wait()` or come back to it later by id. Everything is scoped. A [**user**](https://www.noclick.com/docs/guide/users) is someone your product acts for, a person or a company, known by your own id for them. Inside `noclick.as_user(user)`, every call acts for that user, with their connections, their agents, their conversations and their spend cap. ## Languages Every example in this guide is in Python and TypeScript: pick one in the menu at the top and the whole guide follows. The [TypeScript client](https://www.noclick.com/docs/reference/typescript) runs in Node, Deno, Bun and browsers. ```bash python pip install noclick ``` ```bash typescript npm install noclick ``` Any language can call the [HTTP API](https://www.noclick.com/docs/guide/http-api) directly, and agents in Claude, Cursor or any MCP client can use the platform through its [MCP server](https://www.noclick.com/docs/guide/mcp). ## Getting started 1. Sign in to NoClick and open the [developer console](https://www.noclick.com/dashboard/developers). 2. Create an API key under **API keys**. Creating one accepts the [Developer Terms](https://www.noclick.com/legal/developer-terms) and the [Acceptable Use Policy](https://www.noclick.com/legal/acceptable-use). 3. Install the SDK and set `NOCLICK_API_KEY` in your environment. Then follow [Getting started](https://www.noclick.com/docs/guide/getting-started) to make your first calls, or jump to what you're building: - An assistant your users talk to: [Agents](https://www.noclick.com/docs/guide/agents) - Your own LLM calling your users' apps: [Toolkits](https://www.noclick.com/docs/guide/toolkits) - A workflow that needs a person's sign-off: [Asking people](https://www.noclick.com/docs/guide/humans) - An agent with its own inbox or phone number: [Inboxes](https://www.noclick.com/docs/guide/inboxes), [Phone numbers](https://www.noclick.com/docs/guide/phone-numbers) --- # Getting started Source: https://www.noclick.com/docs/guide/getting-started Create a key, install the SDK and make your first calls. ## Create an API key Open the [developer console](https://www.noclick.com/dashboard/developers) and go to **API keys**. Choose what the key may do (every scope is listed under [API keys and scopes](https://www.noclick.com/docs/guide/keys)), optionally cap what it can spend each month, and create it. The key starts with `nk_live_` and is shown once, so copy it somewhere safe. Creating a key accepts NoClick's [Developer Terms](https://www.noclick.com/legal/developer-terms), which cover using the platform, and its [Acceptable Use Policy](https://www.noclick.com/legal/acceptable-use). Keep the key on your server. For code that runs in a browser, mint a short-lived [session](https://www.noclick.com/docs/guide/sessions) instead. ## Install the SDK The Python SDK needs Python 3.10 or later; the TypeScript client, Node 20 or later (or Deno or Bun). ```bash python pip install noclick export NOCLICK_API_KEY=nk_live_... ``` ```bash typescript npm install noclick export NOCLICK_API_KEY=nk_live_... ``` The SDK reads `NOCLICK_API_KEY` from the environment. `NOCLICK_API_URL` changes the endpoint; it defaults to `https://api.noclick.com`. ## Make your first calls Create a [user](https://www.noclick.com/docs/guide/users), keyed by the id your product already uses for them, then a connection for them to fill: ```python import noclick user = noclick.User.create("user_42", name="Ana Lima", email="ana@example.com") print(user.id, user.console_url) with noclick.as_user(user): gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True) print(gmail.connect_url) # send Ana here ``` ```ts import { Connection, User, asUser } from "noclick"; const user = await User.create("user_42", { name: "Ana Lima", email: "ana@example.com" }); console.log(user.id, user.consoleUrl); await asUser(user, async () => { const gmail = await Connection.fromName("gmail", { integration: "gmail", createIfMissing: true }).hydrate(); console.log(gmail.connectUrl); // send Ana here }); ``` `user.console_url` opens the user in the developer console, where you can see everything done for them. `gmail.connect_url` is a page where Ana connects her Gmail. When she does, `gmail.wait()` returns and `gmail.run(...)` acts in her account. ## Configure the client Most code never builds a client: the SDK makes one from the environment the first time it's needed. To set the key in code, or to use a second account, build one yourself: ```python import noclick noclick.configure(api_key="nk_live_...") # replaces the default client other = noclick.Client(api_key="nk_live_other") noclick.User.list(client=other) # class methods take client= ``` ```ts import { NoClick, User, configure } from "noclick"; configure({ apiKey: "nk_live_..." }); // replaces the default client const other = new NoClick({ apiKey: "nk_live_other" }); await User.list({ client: other }); // static methods take client ``` An object you get back keeps using the client that fetched it. `api_key` can also be a function, called before every request, when your key comes from a secret store that rotates it. ## Blocking or async Every method blocks and returns its result. Each one also has an async form, `.aio`: ```python reply = agent.message("Hi") # blocks reply = await agent.message.aio("Hi") # inside async code ``` ```ts const reply = await agent.message("Hi"); // every call returns a promise; there's no blocking form ``` Both forms work anywhere, including Jupyter and code that already runs an event loop. [Async and concurrency](https://www.noclick.com/docs/guide/async) covers the details. ## Lazy objects `noclick.User.from_id`, `noclick.Connection.from_name`, `noclick.Agent.from_name` and `noclick.Operation.from_id` return an object without calling the API. The call happens the first time you use the object. With `create_if_missing=True`, that first use also creates it if it doesn't exist, so the same line of code works on a fresh account and a busy one: ```python agent = noclick.Agent.from_name("support", create_if_missing=True, instructions="Answer billing questions.") ``` ```ts import { Agent } from "noclick"; const agent = Agent.fromName("support", { createIfMissing: true, instructions: "Answer billing questions." }); ``` ## When something fails Every error the API returns is an `noclick.Error` subclass with a `message`, and usually a `next_step`: one sentence saying what to do about it. ```python try: noclick.User.create("user_42") except noclick.Conflict as err: print(err.existing_id) # "user_42": the id is taken ``` ```ts import { Conflict, User } from "noclick"; try { await User.create("user_42"); } catch (err) { if (!(err instanceof Conflict)) throw err; console.log(err.existingId); // "user_42": the id is taken } ``` [Errors and retries](https://www.noclick.com/docs/guide/errors) lists them all. Transient failures (timeouts, rate limits, most server errors) are retried for you, without doing the work twice. --- # Users Source: https://www.noclick.com/docs/guide/users A user is someone your product acts for. Everything you do for them is scoped to them. A **user** is someone your product acts for: a person or a company. You know them by the id your product already uses for them, and NoClick uses that same id. A user's connected accounts, conversations, inboxes and spend belong to that user and nobody else. ## Create a user Create one when someone signs up to your product, with the id you already use for them: ```python import noclick user = noclick.User.create( "user_42", # your id for them name="Ana Lima", email="ana@example.com", spend_cap_usd_per_month=20, ) ``` ```ts import { User } from "noclick"; const user = await User.create("user_42", { // your id for them name: "Ana Lima", email: "ana@example.com", spendCapUsdPerMonth: 20, }); ``` You don't have to create users up front. `from_id` returns a lazy reference, and with `create_if_missing=True` the user is created the first time it's used. If two requests race to create it, both get the same user: ```python user = noclick.User.from_id("user_42", create_if_missing=True, email="ana@example.com") ``` ```ts import { User } from "noclick"; const user = User.fromId("user_42", { createIfMissing: true, email: "ana@example.com" }); ``` The fields you pass are the user's definition, as with an agent: on a user that already exists, the ones that differ are updated to match, so running this at every sign-in keeps the email you show and the one NoClick emails in step. A field you leave out is never changed, and `None` clears one. ## Where the user is Give a user their timezone, as an IANA name, and every agent acting for them knows what "tomorrow morning" means there: ```python user = noclick.User.from_id("user_42", create_if_missing=True, timezone="America/Sao_Paulo") ``` ```ts import { User } from "noclick"; const user = User.fromId("user_42", { createIfMissing: true, timezone: "America/Sao_Paulo" }); ``` Every turn of an agent acting for the user starts with the current date and time in the agent's own `timezone` if it has one, else the user's, else UTC. A schedule written `noclick.Cron("0 8 * * *", tz="user")` fires at 8am wherever the agent's user is, and follows them when `user.update(timezone=...)` moves them: from its next run, it fires in their new zone. `timezone` converges like the other fields, `None` clears it, a name that isn't an IANA timezone raises `noclick.InvalidRequest`, and a user acting for themselves (their session, or a key pinned to them) may change their own. ## The user's id A user's id is yours. NoClick keeps it exactly as you give it and never issues another, so `user_42` and `auth0|123` work as they are. An id is: - 1 to 255 characters of printable ASCII, so no spaces; - free of `/`; - anything but `all` and `none`, which [list filters](https://www.noclick.com/docs/guide/activity#across-users) use, and `.` or `..`, which URLs resolve away. Any other id raises `noclick.InvalidRequest` naming the rule it breaks. An id is unique among your live users, so creating the same one twice raises `noclick.Conflict`, whose `existing_id` is that id. If your own id for someone changes, change theirs with `user.update(id=...)`. Everything the user has follows them. ## Act for a user Inside `noclick.as_user`, every call acts for that user: ```python with noclick.as_user(user): slack = noclick.Connection.from_name("slack", integration="slack", create_if_missing=True) # Ana's Slack, not yours inbox = noclick.Inbox.create(local_part="ana-support") ``` ```ts import { Connection, Inbox, asUser } from "noclick"; await asUser(user, async () => { const slack = Connection.fromName("slack", { integration: "slack", createIfMissing: true }); // Ana's Slack, not yours const inbox = await Inbox.create({ localPart: "ana-support" }); }); ``` `as_user` takes a `User` or the user's id: `noclick.as_user("user_42")` does the same. To act for a user in a single call, most methods take `user=`, which wins over `as_user`. `as_user` works the same in async code (`async with`), and it follows your code into tasks and `asyncio.to_thread`. A thread you start yourself (with `threading.Thread` or a `ThreadPoolExecutor`) starts without it, and its calls act for your account. Pass `user=` in code that runs on your own threads. Outside `as_user`, calls act for your account itself. Your own connections, agents and inboxes live there. An object keeps acting for the user it came from. A `Connection` you listed inside `as_user(user)` still belongs to that user when you use it later, outside the block. ## Shared agents and a user's own An agent created outside `as_user` belongs to your account and is shared. When a user messages it, the conversation is that user's, but the agent's tools act in your account's connections, never the user's. An agent created inside `as_user` belongs to that user alone and uses that user's connections: give each user their own agent to act in their own accounts. See [Agents](https://www.noclick.com/docs/guide/agents#whose-accounts-it-acts-in). ## A company and its people A user is one boundary: their connections, conversations and data are their own. A company with several people fits two ways, and you choose which in your own data model: - **The company is the user.** Everyone at Acme acts as `acme` and shares its connections; reach a particular person with `noclick.Human(email=...)`. - **Each person is a user, and the company one too.** Dana's `gmail` is hers, and the company's `team-slack` joins her agent's tools by [sharing it](https://www.noclick.com/docs/guide/connections#share-one-across-users). Which people see which company connections is yours to decide: NoClick honours what your account puts in each agent's tools. ## Find users ```python noclick.User.list(q="ana") # prefix of the id, name or email noclick.User.list(status="suspended") page = noclick.User.list_page(limit=50) # one page; pass page.next_cursor back as cursor= user = noclick.User.get("user_42") ``` ```ts import { User } from "noclick"; await User.list({ q: "ana" }); // prefix of the id, name or email await User.list({ status: "suspended" }); const page = await User.listPage({ limit: 50 }); // one page; pass page.nextCursor back as cursor const user = await User.get("user_42"); ``` ## Update, suspend or delete ```python user.update(name="Ana Lima-Souza", phone=None) # None clears a field user.update(id="ana-lima") # a new id; a taken one raises noclick.Conflict user.suspend() user.delete() ``` ```ts await user.update({ name: "Ana Lima-Souza", phone: null }); // null clears a field await user.update({ id: "ana-lima" }); // a new id; a taken one throws Conflict await user.suspend(); await user.delete(); ``` Nothing can act for a suspended user: every call made for them fails with `noclick.PermissionDenied`, whether it names the user or comes from the user's session or a key pinned to them. Their history stays readable. Work already set up for them keeps going, though: their inboxes still receive mail and wake their agents, their phone numbers still answer, and their channels still run. Delete those too if the user must go quiet. Reactivate a user from their page in the console (over HTTP, `PATCH /v1/users/{id}` with `"status": "active"`). Deleting a user marks them deleted and frees their id for reuse, and that's all it does for now: what was set up for them isn't stopped or revoked. From then on no call can act for them (each fails with `noclick.NotFound`), so nothing they hold can be reached to delete. Delete what acts for them first, inside `as_user`: their agents (`noclick.Agent.list(user_filter=user)`), which ends their triggers, and their connections (`noclick.Connection.list()`), which revokes their grants at each provider, as the [assistant example](https://www.noclick.com/docs/examples/assistant-for-every-user) does; then their inboxes, phone numbers and channels if they have any. ### What deleting a user stops, and what it keeps > [!NOTE] > Invite-only beta. [Request access](mailto:support@noclick.com?subject=Invite-only%20beta%20access) to delete users this way; without access, `delete` (with `export=True` too) only marks the user deleted, as above. In the beta, deleting a user leaves nothing acting for them: - their connections' grants are revoked at each provider, so the tokens stop working there too; - their agents' triggers stop: schedules, webhooks, app events and queues no longer wake them; - their inboxes stop receiving mail, and their phone numbers are released (billing stops). What's kept is the record and the data. Their operations, tool calls, conversations, usage and spend stay readable with `user_filter`, for your books, and the data their databases, buckets, repos, volumes and memory hold stays until you delete it. `user.delete(export=True)` hands the user a copy first. ## Export a user's data `user.export()` packs up everything the user holds, from agents and conversations to databases and files, in forms that work without NoClick. `user.delete(export=True)` exports first and deletes the user once the export is stored (if it fails, nothing is deleted): ```python bundle = user.delete(export=True).wait() print(bundle.url) ``` ```ts const bundle = await (await user.delete({ export: true })).wait(); console.log(bundle.url); ``` See [Export](https://www.noclick.com/docs/guide/export) for what the bundle holds. ## Cap a user's spend `spend_cap_usd_per_month` limits what everything done for a user may cost in a calendar month: agent turns, tool calls, sandboxes and messages. Once the user reaches it, new work for them fails with `noclick.SpendCapExceeded`. See [Spend and limits](https://www.noclick.com/docs/guide/spend). ## Pause a user's spending Pausing stops everything that costs money for one user until you resume them, and tells them what they can do about it, in your words. Your billing decides when: pause a user when their balance runs low or a payment fails, and resume them when they pay. NoClick never pauses anyone on its own. [Charging your users](https://www.noclick.com/docs/guide/spend#charging-your-users) shows the whole loop. ```python user = noclick.User.from_id("user_42") user.pause_spending( message="You've used this month's credits.", action=("Top up", "https://acme.com/billing/top-up?user={{user.id}}"), ) print(user.spending.paused, user.spending.since) user.resume_spending() # they paid: what was held for them runs now ``` ```ts import { User } from "noclick"; const user = User.fromId("user_42"); await user.pauseSpending({ message: "You've used this month's credits.", action: { label: "Top up", url: "https://acme.com/billing/top-up?user={{user.id}}" }, }); console.log(user.spending?.paused, user.spending?.since); await user.resumeSpending(); // they paid: what was held for them runs now ``` `message` is what they're told, up to 500 characters, and `action` a `(label, url)` pair for your checkout or billing page: a label up to 40 characters and an `https` URL. Both may use `{{user.id}}`, `{{user.name}}`, `{{user.email}}` and `{{user.metadata.}}`, filled in when you pause; a field the user doesn't have raises `noclick.InvalidRequest`. Leave `message` out and the default you set in the console (Settings, then Paused spending) is used, and leave `action` out for the default's button; with no message and no default either, the call raises `noclick.InvalidRequest`. In a link, each filled-in value is percent-encoded, unless one placeholder is the whole link (`{{user.metadata.checkout_url}}`), which goes in as it is. Keep the link out of `message`: a phone call reads `message` aloud, and everywhere else shows `action` as a button or a link after it. ### While a user is paused Nothing new that costs money starts for them. What happens instead depends on where it would have started: | Where | What happens | | --- | --- | | Your code, acting for them | The call raises `noclick.SpendingPaused` (402) before doing any work. Its `user_message` and `action` are yours to show them; its `message` and `next_step` are for you. | | A message they send an agent: an email to its inbox, a post in their channel | NoClick answers with your message and action, once per conversation while they're paused (a reply to their email, a note in their channel), without running the agent, and holds the message. Mail someone else sends their agents is held too, without a word. NoClick's answers aren't charged. | | A call to a number whose agent works for them, or from them to one that knows them as one of your users | The call is answered, hears your message and ends. A call can't be held. | | Their agents' schedules, webhooks and app events | Skipped, as for a [paused agent](https://www.noclick.com/docs/guide/triggers#pause-an-agent): a scheduled run isn't made up later, a webhook is answered `409` so its sender can retry, and an app event doesn't wake the agent. | | Their agents' queue triggers | Messages wait on the queue with their attempts unchanged, and the agent works through them after you resume. | | A wake-up that comes due | Held, like their messages. | | Work under way when you pause | Finishes the step it's on (the model call or tool run in progress, the reply a call is speaking) and stops: the next tool call doesn't run, nor the next model call on NoClick's key. A turn ends with `spending_paused`, and the user is told where they were talking. | What they already have keeps running and billing: their phone numbers, domains, buckets, volumes and sandboxes. Pausing isn't cancelling: delete what should stop. Everything that costs nothing still works: reading their conversations, runs, files and usage, creating and changing their agents and connections, and writing to their storage. Held messages run when you resume, in the order they arrived, each as the turn it would have been and starting with when it arrived, so the agent knows it waited. A user's held messages are kept for 24 hours, up to 50 at a time; one that comes past either is still answered with your message, and stays in its inbox or channel, but doesn't run. `user.spending.held` is how many are waiting. ### What stops a user's work, in order Pausing is one of four things that can stop work for a user, and they're checked in this order: 1. **Suspended** (`user.suspend()`): nothing acts for them at all, with `noclick.PermissionDenied`. 2. **Spending paused**: your billing's call about this user, with `noclick.SpendingPaused`, and they're told what to do. 3. **A spend cap** reached, the user's, their agent's or the key's, with `noclick.SpendCapExceeded`. 4. **Your account's funds** used up, with `noclick.PaymentRequired`; your users are told only that it isn't available. [Pausing an agent](https://www.noclick.com/docs/guide/triggers#pause-an-agent) is something else: it stops one agent's triggers, whoever it works for. Your account's own work, and visitors to an account agent's [shared page](https://www.noclick.com/docs/guide/sharing), belong to no user, so pausing a user never touches them; a page sharing one of the user's own agents bills that user, so while they're paused its visitors read that the agent isn't available. Only a key acting for your whole account can pause or resume a user, and so can you, on the user's page in the console: never the user's own session or a key pinned to them. Pausing a paused user changes the message and action and keeps `since` and what's held; resuming a user who isn't paused changes nothing. Each pause and resume sends your [webhooks](https://www.noclick.com/docs/guide/webhooks#event-types) `user.spending.paused` or `user.spending.resumed`, and `noclick.User.list(spending="paused")` lists everyone you've paused, to check against your billing. ## Verify a user's phone To prove a user owns a phone number, text them a code and check what they type back: ```python user.verify_phone(phone="+14155550100") try: user.check_phone(code_they_typed) print(user.phone_verified_at) except noclick.InvalidRequest as err: print(err.message) # That code isn't right. ``` ```ts import { InvalidRequest } from "noclick"; await user.verifyPhone({ phone: "+14155550100" }); try { await user.checkPhone(codeTheyTyped); console.log(user.phoneVerifiedAt); } catch (err) { if (!(err instanceof InvalidRequest)) throw err; console.log(err.message); // That code isn't right. } ``` An expired code raises `noclick.Expired`, and too many wrong tries raise `noclick.RateLimited`. ## Ask the user something `user.human` reaches the user at their email address. Ask them something, get their approval, or ask them to connect an account. See [Asking people](https://www.noclick.com/docs/guide/humans). ```python approved = user.human.approve("Send the weekly summary to your team?").wait() ``` ```ts const approved = await (await user.human.approve("Send the weekly summary to your team?")).wait(); ``` --- # Sessions for the browser Source: https://www.noclick.com/docs/guide/sessions Give your frontend a short-lived token that acts as one user, so your secret key never leaves your server. Your API key can do anything in your account, so it stays on your server. When code running in a user's browser needs to call NoClick directly (to run an agent chat, show a connect button, or list their inbox), your server mints a **session**: a token that acts only as that user, with only the scopes you give it, for a short time. ## Mint a session on your server ```python import noclick def session_for(user_id: str) -> dict: user = noclick.User.from_id(user_id) session = user.session(["agents", "humans", "credentials:request", "credentials:use"], ttl_s=3600) return {"token": session.token, "expires_at": session.expires_at.isoformat()} ``` ```ts import { User } from "noclick"; async function sessionFor(userId: string) { const user = User.fromId(userId); const session = await user.session({ scopes: ["agents", "humans", "credentials:request", "credentials:use"], ttlS: 3600 }); return { token: session.token, expires_at: session.expiresAt?.toISOString() }; } ``` Return the token to your frontend, which uses it the way it would use an API key. The token starts with `nk_sess_`, and a session's `repr` never prints it. A session can hold any of these scopes, as long as the key that minted it has them too: | Scope | Lets the browser | | --- | --- | | `agents` | message the user's agents and read their conversations | | `humans` | send the user's own requests and read them | | `credentials:request` | create the user's connections and send their connect links | | `credentials:use` | list and read the user's connections and run operations through them | | `channels` | open, post in and read the user's channels | | `inbox` | create and read the user's inboxes | | `ai` | call [AI models](https://www.noclick.com/docs/guide/ai), charged to your account and counted toward the user's spend cap | Sessions never create keys, sessions or users, and never change policy, brand or the user's spend cap. Those stay with your secret key. A session with `agents` can create agents for its user, though, and set their budgets. And any session may move its own user's [timezone](https://www.noclick.com/docs/guide/users#where-the-user-is), with `user.update(timezone=...)` (`PATCH /v1/users/{id}` with `timezone` alone), so a user who travels can say where they are; it can't read or change anything else about them. ## How long a session lasts `ttl_s` is between 60 seconds and 24 hours, one hour by default. `session.expired` tells you whether it has run out by your machine's clock. Revoking the key that minted a session revokes the session too. Mint a fresh session when your page loads instead of storing one. The call is cheap: `User.from_id` makes no request of its own, so minting a session is a single call. ## Use a session from your server A session can also drive the SDK on your server, which is handy for testing exactly what your frontend is allowed to do: ```python as_ana = noclick.Client.for_session(session) # or session.client() noclick.Agent.list(client=as_ana) # Ana's agents and the shared ones, nothing else ``` ```ts import { Agent, NoClick } from "noclick"; const asAna = NoClick.forSession(session); // or session.client() await Agent.list({ client: asAna }); // Ana's agents and the shared ones, nothing else ``` ## Calling from the browser The API answers cross-origin requests from any origin, with the token as a bearer and no cookies. The [TypeScript client](https://www.noclick.com/docs/reference/typescript) takes the token with `NoClick.forSession(token)`. --- # Brand Source: https://www.noclick.com/docs/guide/brand Show your product's name, logo and colour on the pages and emails NoClick sends your users. When NoClick asks one of your users something for you, the page they answer on and the email that brings them there carry your **brand**, not NoClick's. ```python import noclick noclick.Brand.set( name="Acme", logo_url="https://acme.com/logo.png", color="#FF5500", support_url="https://acme.com/help", email_from_name="Acme Support", ) ``` ```ts import { Brand } from "noclick"; await Brand.set({ name: "Acme", logoUrl: "https://acme.com/logo.png", color: "#FF5500", supportUrl: "https://acme.com/help", emailFromName: "Acme Support", }); ``` - `name` and `logo_url` head the page. - `color` (as `"#RRGGBB"`, or the short `"#RGB"`; it's kept as lowercase `#rrggbb`) is the page's accent. - `support_url` is linked from the page, for people who have a question. - `email_from_name` is the sender name on the emails. An account has one brand. `set` replaces it: a field you leave out is cleared. To change one field and keep the rest, use `update`, where `None` clears a field: ```python noclick.Brand.update(color="#1A73E8") # the name, logo and the rest stay noclick.Brand.update(support_url=None) # no support link from now on ``` ```ts import { Brand } from "noclick"; await Brand.update({ color: "#1A73E8" }); // the name, logo and the rest stay await Brand.update({ supportUrl: null }); // no support link from now on ``` Before you've set a brand, `update` changes NoClick's default one: the name stays `NoClick` until you give it yours. ```python brand = noclick.Brand.get() print(brand.name, brand.is_default) noclick.Brand.reset() # back to NoClick's defaults ``` ```ts import { Brand } from "noclick"; const brand = await Brand.get(); console.log(brand.name, brand.isDefault); await Brand.reset(); // back to NoClick's defaults ``` `is_default` is `True` while you haven't set a brand. ## Serve the pages from your own domain By default the pages live on NoClick's host. With `domain`, the approval, question and hand-off pages your users see are served from a host of yours, and the emails that bring them there link to it and name only your brand: ```python noclick.Brand.update(domain="connect.acme.com") ``` ```ts import { Brand } from "noclick"; await Brand.update({ domain: "connect.acme.com" }); ``` `domain` is a [domain](https://www.noclick.com/docs/guide/domains) your account holds, or a subdomain of one, that serves nothing else. A bought domain gets its `CNAME` record at once. A connected one lists the records to add in its `records`: the `CNAME`, and a `TXT` that proves the name is yours. The domain's `serves` then includes `brand`. Requests made once the records are live and the host's certificate is issued link to it; links already sent keep working, and their pages open on your host too. The emails still come from NoClick's address, under your `email_from_name`, and connect links stay on NoClick's host for now. `domain=None` moves the pages back to NoClick's host, and the host stops serving them. ## Use your own OAuth apps When a user connects Google, Slack or another service, the consent screen names the OAuth app asking for access: NoClick's, unless you bring your own. Create the app at the provider with the redirect URI your brand lists for it, keep its client id and secret in an account [secret](https://www.noclick.com/docs/guide/secrets) under `CLIENT_ID` and `CLIENT_SECRET`, and name it per provider: ```python print(noclick.Brand.get().oauth_redirect_urls["google"]) # register this as the app's redirect URI google_app = noclick.Secret.from_name("google-oauth-app", create_if_missing=True, values={ "CLIENT_ID": "1234-abc.apps.googleusercontent.com", "CLIENT_SECRET": "...", }) noclick.Brand.update(oauth_apps={"google": google_app}) ``` ```ts import { Brand, Secret } from "noclick"; console.log((await Brand.get()).oauthRedirectUrls.google); // register this as the app's redirect URI const googleApp = Secret.fromName("google-oauth-app", { createIfMissing: true, values: { CLIENT_ID: "1234-abc.apps.googleusercontent.com", CLIENT_SECRET: "...", } }); await Brand.update({ oauthApps: { google: googleApp } }); ``` The consent screen then shows your app's name and logo, which some scopes need anyway: a provider may ask that restricted scopes, such as reading all of a user's mail, come from an app that has passed its own review. You can bring an app for Asana, Box, ClickUp, GitLab, Google (Gmail, Calendar, Drive and the rest of Google's services), Intercom, Klaviyo, monday.com, PagerDuty, Pipedrive, Slack, Webflow and Zoom; `oauth_redirect_urls` has the redirect URI for each. Naming any other provider raises `noclick.InvalidRequest`, and so does a secret that isn't your account's or lacks either key. Connect links made after you name an app sign in with it, and each connection keeps the app it was made with: its access refreshes on that app too, so naming another app later, or deleting the secret, changes only the links made after. Connections made before keep NoClick's app until they're reconnected. A link made with an app whose secret has since been deleted, or lost a key, can't connect that provider; make a new link. `update` changes only the providers you name: map one to `None` and links made after sign in with NoClick's app again, or pass `oauth_apps=None` to remove them all. `set` replaces the whole brand, `oauth_apps` included. ```python noclick.Brand.update(oauth_apps={"slack": noclick.Secret.from_name("slack-oauth-app")}) # Google's stays noclick.Brand.update(oauth_apps={"google": None}) # new Google links use NoClick's app ``` ```ts import { Brand, Secret } from "noclick"; await Brand.update({ oauthApps: { slack: Secret.fromName("slack-oauth-app") } }); // Google's stays await Brand.update({ oauthApps: { google: null } }); // new Google links use NoClick's app ``` --- # Connections Source: https://www.noclick.com/docs/guide/connections Name each account your code and agents act in, let your users connect it once, and use it by name. A **connection** is one account on another service, under a name you choose: Ana's Gmail as `gmail`, Acme's Stripe as `stripe`. Your code, your toolkits and your agents name the connection, so they always act in exactly that account. Your users connect their own accounts through a page NoClick hosts, and you never see or store their tokens. NoClick keeps OAuth tokens fresh and checks that each connection works. ## Create a connection ```python import noclick with noclick.as_user("user_42"): gmail = noclick.Connection.from_name( "gmail", integration="gmail", create_if_missing=True, reason="to send your weekly summaries", redirect_url="https://app.example.com/settings/connections", ) print(gmail.status, gmail.connect_url) # pending https://noclick.com/credential/provide/... ``` ```ts import { Connection, asUser } from "noclick"; await asUser("user_42", async () => { const gmail = await Connection.fromName("gmail", { integration: "gmail", createIfMissing: true, reason: "to send your weekly summaries", redirectUrl: "https://app.example.com/settings/connections", }).hydrate(); console.log(gmail.status, gmail.connectUrl); // pending https://noclick.com/credential/provide/... }); ``` A new connection is **pending**: no account is connected yet, and `connect_url` is the page where the user signs in to the service. Show it in your product (a "Connect Gmail" button is typical). `reason` appears on the page, so tell the user why you're asking. When they finish, NoClick sends them to your `redirect_url` with `status=success` in the query string. An operation that needs no account, like Exa's web search, needs no connection at all: name it `"exa.search"` in tools, or run it with `noclick.run`. A connection named `exa` holding your own key makes the same names use your key: see [Operations on NoClick's key](https://www.noclick.com/docs/guide/integrations#operations-on-noclick-s-key). `from_name` is lazy: nothing is sent until you use the connection, and with `create_if_missing=True` that first use creates it if it doesn't exist, so the same line works for a new user and a returning one. `noclick.Connection.create` always creates, and raises `noclick.Conflict` if the name is taken. A name is 1 to 32 lowercase letters, digits, `-` and `_`, unique for each user (and among your account's own connections). `search`, `fetch` and `code` are the built-in tools' names and can't be used. ## Two accounts on one service A user with two Gmail accounts has two connections: ```python with noclick.as_user("user_42"): work = noclick.Connection.from_name("work-gmail", integration="gmail", create_if_missing=True) home = noclick.Connection.from_name("home-gmail", integration="gmail", create_if_missing=True) ``` ```ts import { Connection, asUser } from "noclick"; await asUser("user_42", async () => { const work = Connection.fromName("work-gmail", { integration: "gmail", createIfMissing: true }); const home = Connection.fromName("home-gmail", { integration: "gmail", createIfMissing: true }); }); ``` A toolkit or agent given both gets both sets of tools and picks per call. ## Wait for the user ```python gmail.wait() # returns once an account is connected print(gmail.account) # ana@example.com, when the provider says who it is ``` ```ts await gmail.wait(); // returns once an account is connected console.log(gmail.account); // ana@example.com, when the provider says who it is ``` `wait()` returns at once when the connection is already connected. It waits on the connection, not on one link: an account connected through any of its links ends the wait, and when a link lapses after you made a fresh one (`connect_link()` or `request()`, from any process), it keeps waiting on the fresh one. It raises `noclick.OperationExpired` when the open link lapses and no fresh one was made. In a web server, don't block a request on it: check `gmail.refresh().connected` later, or wait in a background job. ## Send the link for you `request()` emails a fresh link to the user. `connect_link()` makes one without sending it, or sends it where `send_to` says: `"user"`, or, only from code that isn't acting for a user, `"owner"` (your account's owner) or another email address. A connection of your account's own has no user, so `request()` on one takes `send_to="owner"` or an email; its default, `"user"`, raises `noclick.InvalidRequest`. ```python link = gmail.request(reason="to post your team's standup") print(link.url) print(link.delivered_via) # ["email"] print(link.undelivered) # channels that failed, each with a reason ``` ```ts const link = await gmail.request({ reason: "to post your team's standup" }); console.log(link.url); console.log(link.deliveredVia); // ["email"] console.log(link.undelivered); // channels that failed, each with a reason ``` Links go out by email only. For a user with no email address, `send_to` raises `noclick.InvalidRequest` before anything is created: make the link without `send_to` and pass `link.url` on yourself. The new link is the connection's own from then on, so `gmail.connect_url` is its `url`. A link is also an [operation](https://www.noclick.com/docs/guide/operations): `link.wait()` returns the connected `Connection`. ## Use it ```python gmail.run("send_email_message", { "to": ["ana@example.com"], "subject": "Your weekly summary", "body": "Three invoices were paid this week.", }) ``` ```ts await gmail.run("send_email_message", { to: ["ana@example.com"], subject: "Your weekly summary", body: "Three invoices were paid this week.", }); ``` [Calling integrations](https://www.noclick.com/docs/guide/integrations) covers what a connection can do (`operations()` and `operation()`), `run` and `lookup`. To hand a connection to a model, list it in a [toolkit](https://www.noclick.com/docs/guide/toolkits) or in an [agent's tools](https://www.noclick.com/docs/guide/agents). A name there is a connection in the scope the tools run in, else the integration of that name, limited to its operations that need no account. ## Share one across users A connection belongs to one scope, a user or your account, and a name in an agent's tools always means the agent's own user's connection. To give an agent a connection from another scope, pass the `Connection` object itself: it stays that connection, wherever the agent lives. That's how you build what your product needs on top of users: a company whose people share its Slack, or your own support mailbox in every user's agent. ```python import noclick team_slack = noclick.Connection.from_name("team-slack", user="acme") # the company's, connected once with noclick.as_user("dana"): assistant = noclick.Agent.from_name( "assistant", create_if_missing=True, instructions="You help Dana with her email, and post updates to her team.", tools=["gmail", team_slack.only("send_message_to_channel")], ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; const teamSlack = Connection.fromName("team-slack", { user: "acme" }); // the company's, connected once await asUser("dana", async () => { const assistant = Agent.fromName("assistant", { createIfMissing: true, instructions: "You help Dana with her email, and post updates to her team.", tools: ["gmail", teamSlack.only("send_message_to_channel")], }); }); ``` `"gmail"` is Dana's own connection; `team_slack` stays Acme's. Your account's own connections share the same way: ```python import noclick support_mail = noclick.Connection.from_name("support-gmail") # your account's with noclick.as_user("user_42"): assistant = noclick.Agent.from_name( "assistant", create_if_missing=True, instructions="When the user needs a person, email our support team.", tools=[support_mail.only("send_email_message"), "search"], ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; const supportMail = Connection.fromName("support-gmail"); // your account's await asUser("user_42", async () => { const assistant = Agent.fromName("assistant", { createIfMissing: true, instructions: "When the user needs a person, email our support team.", tools: [supportMail.only("send_email_message"), "search"], }); }); ``` What holds when you share one: - **Only your account decides.** Only a key that acts for your whole account (the `NoClick-User` header is fine) can put another scope's connection in an agent's, a toolkit's or a hosted MCP server's tools, or make it an agent's `model_connection`. A key pinned to a user, a [session](https://www.noclick.com/docs/guide/sessions), an app's code or an agent's own NoClick tools raises `noclick.PermissionDenied`, before anything is looked up, so no user (and no model) can reach another's accounts on their own. They may keep what you gave, or narrow it (one of its operations, an argument bound), never widen it, and a spawned child gets it only as its parent holds it. - **The agent's user never gets the connection.** Not by its id, not its token, not a run of one of its operations, and not its connect link: while it's pending or disconnected, its tools tell the agent it's unavailable, and its own user reconnects it. - **Tool names stay unique.** The model tells a shared connection apart by its name (in a toolkit, `team-slack__send_message_to_channel`), so an agent or a toolkit can't hold two connections of one name: name company connections apart, like `team-slack`. - **The owner approves.** A call is judged by your account's [approval rules](https://www.noclick.com/docs/guide/policy) and by the rules of the agent's user and the connection's user, and the strictest one decides. A held call is approved by the connection's owner: its user, or your account's owner for your account's connection. - **The agent's user pays.** Its calls count toward the spend cap of the user the agent acts for, and show in both users' activity (`noclick.ToolCall.list(user_filter=...)` for either). - **Taking it back.** Take it out of the tools, or delete or reconnect the connection itself. `used_by` on a connection lists every agent and hosted MCP server that has it in their tools, from any scope. A toolkit takes the same objects. A toolkit you build once and use for every user keeps resolving names among each user's connections, while a shared connection stays itself: see [Toolkits](https://www.noclick.com/docs/guide/toolkits). ## Fix arguments the model mustn't choose An agent that reads email can be told, by an email, to send your user's data somewhere else. `bind(...)` on some of a connection's operations takes that choice away: a bound parameter leaves the tool's schema, so the model never sees or sets it, and NoClick fills it on every call. ```python import noclick with noclick.as_user("user_42"): gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True) assistant = noclick.Agent.from_name( "assistant", create_if_missing=True, instructions="You read the user's inbox and email them a summary each morning.", tools=[ gmail.only("fetch_emails_from_inbox"), gmail.only("send_email_message").bind(to="{{user.email}}"), ], ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; await asUser("user_42", async () => { const gmail = Connection.fromName("gmail", { integration: "gmail", createIfMissing: true }); const assistant = Agent.fromName("assistant", { createIfMissing: true, instructions: "You read the user's inbox and email them a summary each morning.", tools: [ gmail.only("fetch_emails_from_inbox"), gmail.only("send_email_message").bind({ to: "{{user.email}}" }), ], }); }); ``` A value is fixed (`channel="#support"`), or names the user the call acts for: `{{user.id}}`, `{{user.email}}`, `{{user.name}}` or `{{user.metadata.}}`, from the [user](https://www.noclick.com/docs/guide/users) record. Anything else in `{{...}}` raises `ValueError` before anything is sent. What holds: - **A bound value always wins**, over what the model passes through in an operation's free-form parameters too (a bound key in an object the operation passes on as is, like Stripe's `extra_params`, is dropped). - **A tool whose binding can't be filled isn't offered** that turn: a user with no email gets no `send_email_message`, and an account conversation has no `{{user...}}` at all. - **Policy judges the final arguments**, bound ones included, so a [rule's condition](https://www.noclick.com/docs/guide/policy#rules-with-conditions) sees `args.to` as it will be sent. - **It works wherever tools do**: an agent's `tools`, a [toolkit](https://www.noclick.com/docs/guide/toolkits) (which sends its entries with each call) and a [hosted MCP server](https://www.noclick.com/docs/guide/mcp-servers), whose `{{user...}}` is the server's user, so an account server doesn't serve a tool bound to one. A shared connection binds the same way: `team_slack.only("send_message_to_channel").bind(channel="#announcements")`. Every operation you list must take each parameter you bind (else `noclick.InvalidRequest`); `bind` again to add more. A bound operation is that operation's only entry: listing `gmail` (every operation, unbound) beside `gmail.only("send_email_message").bind(...)` raises `noclick.InvalidRequest`, so list the others one by one. On the wire each is `{"tool": "gmail.send_email_message", "bind": {"to": "{{user.email}}"}}`, and an agent's `tools` reads it back that way. [Databases](https://www.noclick.com/docs/guide/databases#use-it-from-an-app-or-an-agent) and [buckets](https://www.noclick.com/docs/guide/storage#use-it-from-an-app-or-an-agent) bind their operations the same way, to pin an agent to one table or one folder. A repository on a git host (a `github` connection) can also be checked out in an agent's workspace, with pushes limited to the branches you name: see [Work in a repo from an agent](https://www.noclick.com/docs/guide/repos#work-in-a-repo-from-an-agent). ## See what a user has connected ```python with noclick.as_user("user_42"): for connection in noclick.Connection.list(): print(connection.name, connection.integration, connection.status) ``` ```ts import { Connection, asUser } from "noclick"; await asUser("user_42", async () => { for (const connection of await Connection.list()) { console.log(connection.name, connection.integration, connection.status); } }); ``` Outside `as_user`, `noclick.Connection.list()` lists your account's own connections. To read every user's at once, pass `user_filter=noclick.ALL_USERS`; `integration="gmail"` narrows the list. `noclick.Connection.get` takes an id or a name. A user's connections are private to them. They don't show up among your own connections in the NoClick app. ## Accounts connected in the app The accounts you connected in the NoClick app are connections too, so everything your account can use is one list. Outside `as_user`, `noclick.Connection.list()` ends with them, after your named connections: each one's `origin` is `"app"` (a named connection's is `"api"`), its `name` is the one it has in the app, and its id is `credential:` followed by its id, which is how you refer to it: ```python for connection in noclick.Connection.list(origin="app"): print(connection.id, connection.name, connection.integration, connection.access) slack = noclick.Connection.from_id("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e") slack.run("send_message_to_channel", {"channel": "#ops", "text": "Deployed."}) noclick.run("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e.list_channels") ``` ```ts import { Connection, run } from "noclick"; for (const connection of await Connection.list({ origin: "app" })) { console.log(connection.id, connection.name, connection.integration, connection.access); } const slack = Connection.fromId("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e"); await slack.run("send_message_to_channel", { channel: "#ops", text: "Deployed." }); await run("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e.list_channels"); ``` They run, look up, verify and [require approval](https://www.noclick.com/docs/guide/connections#require-approval-for-an-operation) like any connection, billed and recorded the same way. The app connects, reconnects and removes them, so `connect_link`, `import_` and `delete` on one raise `noclick.InvalidRequest`. An account a teammate shared with yours is listed too, with `access` `"view"` or `"edit"` (`"owner"` for your own): it runs, but its `token()` and its approval rules stay its owner's. They belong to your account, never to one of your users: a call made with `as_user` doesn't see them. An account agent takes one as a tool, the `Connection` itself or its id in `tools`, whole or one operation at a time: ```python slack = noclick.Connection.from_id("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e") ops = noclick.Agent.from_name( "ops", create_if_missing=True, instructions="You post deploy notes for the team.", tools=[slack.only("send_message_to_channel"), "credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e.list_channels"], ) ``` ```ts import { Agent, Connection } from "noclick"; const slack = Connection.fromId("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e"); const ops = Agent.fromName("ops", { createIfMissing: true, instructions: "You post deploy notes for the team.", tools: [slack.only("send_message_to_channel"), "credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e.list_channels"], }); ``` Each turn checks it again: once it's removed in the app or no longer shared with your account, its tools tell the agent they can't run, and once it's disconnected, that it needs reconnecting in the app. A user's agent can't take one, and a [toolkit](https://www.noclick.com/docs/guide/toolkits) names connections only, since its tools go on the wire by a connection's name. ## Require approval for an operation A connection can require its owner to approve each call of an operation, wherever the account is used: your code, your agents, and the workflows in the app. `require_approval` adds operations, or `"*"` for every one, and never removes any; lifting one is done in the app: ```python gmail = noclick.Connection.from_name("gmail") gmail.require_approval("send_email_message", "delete_message") print(gmail.approval_operations) # ['delete_message', 'send_email_message'] ``` ```ts import { Connection } from "noclick"; const gmail = Connection.fromName("gmail"); await gmail.requireApproval("send_email_message", "delete_message"); console.log(gmail.approvalOperations); // ['delete_message', 'send_email_message'] ``` It needs an account to hold the rules: on a pending connection it raises `noclick.InvalidRequest`, so connect it first. A held call raises `noclick.ApprovalRequired`, as a [policy](https://www.noclick.com/docs/guide/policy) hold does, and runs once its owner approves that exact call. Use a policy for rules about tools across your account; use this for one connected account that should never act unseen, whoever uses it. ## From an app's preview An app's [preview](https://www.noclick.com/docs/guide/apps#preview-a-branch) only reads through its connections. An operation that may write in the account (anything not named as a read, such as `list_`, `get_` or `search_`) raises `noclick.PermissionDenied`, naming the fix, so trying a branch never posts to a real channel or emails a real customer. Operations that run on NoClick's key act in no account of yours and run as usual. Production is unchanged. When a connection holds a test account (a sandbox workspace, a test inbox), mark it, and previews write through it as production does: ```python slack = noclick.Connection.from_name("slack-sandbox") slack.mark_test_account() print(slack.test_account) # True ``` ```ts import { Connection } from "noclick"; const slack = Connection.fromName("slack-sandbox"); await slack.markTestAccount(); console.log(slack.testAccount); // true ``` `mark_test_account(False)` takes the mark back. Only the account that connected it marks it, and only once it holds an account. ## Check that it works `status` is `pending` until an account is connected, `connected` after, and `disconnected` once the account is revoked or its credential deleted. A pending connection still runs its [operations on NoClick's key](https://www.noclick.com/docs/guide/integrations#operations-on-noclick-s-key). A connected account can still fail (a revoked grant, a key for the wrong workspace), so `verify()` asks the provider now: ```python check = gmail.verify() print(check.reachable, check.message) # True Signed in as ana@example.com. ``` ```ts const check = await gmail.verify(); console.log(check?.reachable, check?.message); // true Signed in as ana@example.com. ``` `reachable` is `True` when the provider accepted the account, `False` when it refused it, and `None` when NoClick can't tell. `samples` holds a few things from the account, such as channel or repository names, and `message` says what was found. The result is also kept as `gmail.verification`, which stays `None` for a connection nobody has checked yet. There's nothing to ask about without an account: `verify()` on a pending connection raises `noclick.NeedsConnection`, and on a disconnected one `noclick.CredentialDisconnected`, each with the `connect_url` that fixes it (a connection of another scope's, say the account's from a user's code, raises `noclick.NotAvailable`). ## Reconnect A disconnected connection keeps its name. Send a new link, and once the user connects, the connection holds the new account and NoClick removes the old one. Everything that names the connection keeps working: ```python if gmail.refresh().status == "disconnected": gmail.request(reason="Gmail stopped working. Reconnect it to keep your summaries coming.") ``` ```ts if ((await gmail.refresh()).status === "disconnected") { await gmail.request({ reason: "Gmail stopped working. Reconnect it to keep your summaries coming." }); } ``` A run through a disconnected connection raises `noclick.CredentialDisconnected`, which carries the reconnect link as `connect_url`. ## Bring credentials you already hold If you already have a user's API key or OAuth tokens, create the connection with them. NoClick validates the fields and probes the provider before saving, so a rejected key leaves nothing behind: ```python crm = noclick.Connection.from_name( "crm", integration="affinity", data={"api_key": "..."}, create_if_missing=True, user="user_42", ) ``` ```ts import { Connection } from "noclick"; const crm = Connection.fromName("crm", { integration: "affinity", data: { api_key: "..." }, createIfMissing: true, user: "user_42", }); ``` Like any `from_name`, this creates the connection holding that key the first time and uses the existing one after (it never overwrites a key). `noclick.Connection.create(..., data=...)` always creates. `import_` swaps in a new key later, a rotated key for example: `crm.import_({"api_key": "..."})`. When a service takes more than one kind of credential, `credential_type=` says which one you hold; [`Connection.integrations`](https://www.noclick.com/docs/reference/python/Connection#Connection.integrations) lists the types each integration accepts as `credential_types`. ## WhatsApp A WhatsApp connection (`whatsapp_qr`) is linked by QR code: its connect page shows a code, and the person scans it on the phone with that number, in WhatsApp under Linked devices → Link a device, as they would to open WhatsApp Web. It works the same with the WhatsApp and the WhatsApp Business app, keeps the number they already use, and leaves WhatsApp working on their phone. NoClick isn't told which app it is, or whether the account is a person's or a business's, so nothing about the connection says either. Through it an agent reads the account's chats and sends as it, and an `OnEvent` trigger on `receive_message` wakes the agent for each message that arrives. A key (`whatsapp_access_token`) instead connects a number on Meta's WhatsApp Business Platform, the API a business sets up in a Meta developer app (a phone number ID and an access token). It sends, templates included, and manages the business profile, but reads no chats, and NoClick wakes agents on incoming messages only on a QR connection. ## Model providers A model provider's connection holds what an agent on a [coding harness](https://www.noclick.com/docs/guide/agents#harnesses) thinks with: an API key, or a Claude or ChatGPT plan. It's a connection like any other, of an integration whose `kind` is `"model"` (`anthropic`, `openai`, `openrouter`, `opencode` and more): `noclick.Connection.integrations(kind="model")` lists them. The agent names it as its `model_connection`, never in its `tools`. ```python claude = noclick.Connection.create("claude", integration="anthropic", data={"api_key": "sk-ant-..."}) print(claude.verification) # Anthropic accepted the key ``` ```ts import { Connection } from "noclick"; const claude = await Connection.create("claude", { integration: "anthropic", data: { api_key: "sk-ant-..." } }); console.log(claude.verification); // Anthropic accepted the key ``` `data={"api_key": ...}` holds your key, probed first like any key. Without `data`, the connection is pending and its link asks for the key. Where the provider's `subscription` is true, the link also offers signing in with a plan instead, on your account's connection as on a user's: a Claude plan runs only `claude_code`, and a ChatGPT plan runs `codex`, `opencode` and `hermes` on the models the plan allows. A plan connects only at the link, signed in by the person it belongs to, and a connection holding one is never shared with another scope's agent. Whether a plan fits is yours to judge ([when a plan fits](https://www.noclick.com/docs/guide/connections#when-a-plan-fits)): ```python with noclick.as_user("user_42"): plan = noclick.Connection.from_name("claude", integration="anthropic", create_if_missing=True) print(plan.connect_url) # "Paste an Anthropic key" or "Sign in with Claude" ``` ```ts import { Connection, asUser } from "noclick"; await asUser("user_42", async () => { const plan = await Connection.fromName("claude", { integration: "anthropic", createIfMissing: true }).hydrate(); console.log(plan.connectUrl); // "Paste an Anthropic key" or "Sign in with Claude" }); ``` NoClick never hands out a model key or plan token: `token()` on a model provider's connection raises `noclick.PermissionDenied`. > [!NOTE] > Invite-only beta. [Request access](mailto:support@noclick.com?subject=Invite-only%20beta%20access) to use it; without access, a call answers 404. A user's agent can think with your account's key: pass the account's `Connection` object as its `model_connection`, as you would [share one in its tools](https://www.noclick.com/docs/guide/connections#share-one-across-users). The same rules hold: only a key that acts for your whole account can set it, and a connection holding someone's plan is never shared, so a user's agent runs on your key or its user's own plan, never on another person's. The model calls are paid on that key; NoClick bills the agent's user for the rest of the turn. ```python import noclick claude = noclick.Connection.from_name("claude") # your account's Anthropic key with noclick.as_user("user_42"): coder = noclick.Agent.from_name( "coder", create_if_missing=True, instructions="You fix the user's site.", harness="claude_code", model_connection=claude, ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; const claude = Connection.fromName("claude"); // your account's Anthropic key await asUser("user_42", async () => { const coder = Agent.fromName("coder", { createIfMissing: true, instructions: "You fix the user's site.", harness: "claude_code", modelConnection: claude, }); }); ``` ### When a plan fits A Claude or ChatGPT plan is one person's own subscription, so it suits agents that work for that person: your own assistant or coding agent, or an agent answering your own business's messages, on your account's connection; a user's agent on that user's plan, in their scope. Use an API key instead when the agent serves your product's many users, when the plan belongs to someone other than the person the agent works for, or when the provider's terms for that plan wouldn't cover the use. NoClick doesn't decide this for you: an agent runs on whatever its connection holds. An agent setting one up for someone says which fits and why, and goes with their choice. ## Call the provider yourself To call a provider's API directly with a user's OAuth connection, get a short-lived access token. This needs a key with the `credentials:token` scope: ```python token = gmail.token() headers = {"Authorization": f"{token.token_type} {token.access_token}"} ``` ```ts const token = await gmail.token(); const headers = { Authorization: `${token.tokenType} ${token.accessToken}` }; ``` You get access tokens only, never refresh tokens or stored API keys. For API-key connections, use [operations](https://www.noclick.com/docs/guide/integrations) instead. Like `verify()`, it needs an account: a pending connection raises `noclick.NeedsConnection` and a disconnected one `noclick.CredentialDisconnected`, each with its `connect_url`. ## Delete ```python gmail.delete() ``` ```ts await gmail.delete(); ``` This deletes the connection and, where the provider supports it, disconnects NoClick from the account. Code and agents that name it stop working until a connection with that name exists again, and any agent it was [granted](https://www.noclick.com/docs/guide/grants) to loses its tools. `used_by` says beforehand what loses it: the agents holding it (in their `tools`, as their `model_connection`, or [granted](https://www.noclick.com/docs/guide/grants) it by their user), the [hosted MCP servers](https://www.noclick.com/docs/guide/mcp-servers) with it in their tools, and the [apps](https://www.noclick.com/docs/guide/apps) that bind it, each with its `kind` (`agent`, `mcp_server` or `project`), `id`, `name`, `user_id` and an app's `binding` name: ```python gmail = noclick.Connection.from_name("gmail", user="user_42") for holder in gmail.used_by: print(holder["kind"], holder["name"]) gmail.delete() ``` ```ts import { Connection } from "noclick"; const gmail = await Connection.fromName("gmail", { user: "user_42" }).hydrate(); for (const holder of gmail.usedBy) { console.log(holder.kind, holder.name); } await gmail.delete(); ``` --- # Calling integrations Source: https://www.noclick.com/docs/guide/integrations Find a service, see what it can do, and run its operations, including the ones that need no account and no connection. An **integration** is a service NoClick can operate, like `gmail`, `slack` or `hubspot`. Each has **operations**, such as `send_email_message` or `send_message_to_channel`. An operation that needs an account runs through a [connection](https://www.noclick.com/docs/guide/connections), so it always acts in exactly that account. The few that need no account, like a web search, need no connection either: they run on NoClick's own key. ## Find a service ```python import noclick for integration in noclick.Connection.integrations("mail"): print(integration.name, integration.label, integration.connect_with, integration.operation_count) ``` ```ts import { Connection } from "noclick"; for (const integration of await Connection.integrations("mail")) { console.log(integration.name, integration.label, integration.connectWith, integration.operationCount); } ``` `Connection.integrations()` lists every integration NoClick can operate. Pass a query to keep the ones whose name or label contains it, in any case. `account_free_operation_count` says how many of its operations need no account, which is 0 for most. `connect_with` lists the ways a person connects an account, one or more of: `"oauth"` when they sign in to the service, `"key"` when they paste a key or token, `"qr"` when they scan a code, and `"purchase"` for a phone number, which you buy rather than connect. A service that takes either a sign-in or a key lists both (`["oauth", "key"]`), so test with `"oauth" in integration.connect_with`. `credential_types` names the exact credentials an integration accepts, the value `credential_type=` takes when you [bring a key you hold](https://www.noclick.com/docs/guide/connections#bring-credentials-you-already-hold). ## See what it can do ```python slack = noclick.Connection.from_name("slack", integration="slack", user="user_42") for op in slack.operations(search="message"): print(op.name, op.read_only, op.description) send = slack.operation("send_message_to_channel") print(send.parameters) # a JSON schema of its arguments ``` ```ts import { Connection } from "noclick"; const slack = Connection.fromName("slack", { integration: "slack", user: "user_42" }); for (const op of await slack.operations({ search: "message" })) { console.log(op.name, op.readOnly, op.description); } const send = await slack.operation("send_message_to_channel"); console.log(send.parameters); // a JSON schema of its arguments ``` `operations()` lists what the connection's integration can do. `search` keeps the operations whose name or description contains it, in any case. `operation()` reads one by name, and raises `noclick.NotFound` for a name the integration doesn't have. Both read the catalog, not the account. A connection named with `integration=`, like `slack` here, isn't loaded or created to answer them, so they work before the user has connected anything, and before the connection even exists. Without `integration=`, the connection is loaded first to learn which integration it is. Each operation has: - `parameters`: a JSON schema of its arguments. - `read_only`: a hint from the operation's name (reads start with `list_`, `get_`, `fetch_`, `search_` and the like), not a guarantee that nothing changes. - `needs_account`: `False` when it runs on NoClick's key with no account at all, and so with no connection. An operation that needs an account only for some arguments says `True`. - `scopes`: the OAuth scopes it needs, where NoClick knows them. ## Run an operation ```python gmail = noclick.Connection.from_name("gmail", user="user_42") result = gmail.run("send_email_message", { "to": ["ana@example.com"], "subject": "Your weekly summary", "body": "Three invoices were paid this week.", }) ``` ```ts import { Connection } from "noclick"; const gmail = Connection.fromName("gmail", { user: "user_42" }); const result = await gmail.run("send_email_message", { to: ["ana@example.com"], subject: "Your weekly summary", body: "Three invoices were paid this week.", }); ``` `run` returns the operation's data, what the service answered, as plain data. The connection decides whose account it runs in: here, the `gmail` connection of user `user_42`. When the service refuses the call (a channel that doesn't exist, an account without access), `run` raises `noclick.UpstreamError` in the service's own words, with its HTTP status as `upstream_status`. `noclick.run` does the same by ref, the way an agent's tools name it: `noclick.run("gmail.send_email_message", {...}, user="user_42")` runs through that user's `gmail` connection. ## Operations on NoClick's key Some operations need no account, because they run on NoClick's own key: a web search, public data. You run them by name. There's no connection to create, and nothing for anyone to connect: ```python results = noclick.run("exa.search", {"query": "invoice automation for small businesses"}) ``` ```ts import { run } from "noclick"; const results = await run("exa.search", { query: "invoice automation for small businesses" }); ``` A ref is `"name.operation"`. When no connection has that name, `exa` means the integration, and its operations that need no account run on NoClick's key. The same ref works as a tool, for your account's agents and for every user's: ```python with noclick.as_user("user_42"): researcher = noclick.Agent.from_name( "researcher", create_if_missing=True, instructions="You look companies up on the web before a sales call.", tools=["exa.search"], ) ``` ```ts import { Agent, asUser } from "noclick"; await asUser("user_42", async () => { const researcher = Agent.fromName("researcher", { createIfMissing: true, instructions: "You look companies up on the web before a sales call.", tools: ["exa.search"], }); }); ``` A bare `"exa"` gives the agent every Exa operation that needs no account. `account_free_operation_count` says how many an integration has, and `needs_account` on each operation says which. An operation that needs an account doesn't run this way: `noclick.run("exa.create_monitor", {...})` raises `noclick.InvalidRequest`, telling you to create a connection named `exa`. Runs on NoClick's key are billed to your account like any other [tool run](https://www.noclick.com/docs/guide/spend), and count toward the acting user's spend cap. Your [policy](https://www.noclick.com/docs/guide/policy) judges them by the same names, like `exa.search`. ### Bring your own key To run them on your own key, create a connection named `exa` that holds it: ```python noclick.Connection.create("exa", credential_type="exa_api_key", data={"api_key": "..."}) ``` ```ts import { Connection } from "noclick"; await Connection.create("exa", { credentialType: "exa_api_key", data: { api_key: "..." } }); ``` A connection with the integration's name wins. From then on the same refs use it: `noclick.run("exa.search", {...})` and `tools=["exa.search"]` run on your key, and so do Exa's operations that need an account. Create it inside `as_user` to give one user a key of their own. ### A pending connection A connection nobody has connected yet still runs its operations that need no account, on NoClick's key. The rest raise `noclick.NeedsConnection` until it's connected, as the next section shows. ## When nothing is connected If the connection is still pending and the operation needs an account, `run` raises `noclick.NeedsConnection`. The error carries the connection's link and the operation that finishes when the user connects: ```python try: gmail.run("send_email_message", args) except noclick.NeedsConnection as err: notify_user(err.connect_url) gmail.wait() # returns once they've connected gmail.run("send_email_message", args) ``` ```ts import { NeedsConnection } from "noclick"; try { await gmail.run("send_email_message", args); } catch (err) { if (!(err instanceof NeedsConnection)) throw err; notifyUser(err.connectUrl); await gmail.wait(); // returns once they've connected await gmail.run("send_email_message", args); } ``` If the account was revoked at the provider, `run` raises `noclick.CredentialDisconnected` instead, with a link to reconnect as `connect_url`. Both errors name the `connection` and its `integration`. ## Look up IDs Many operations take an ID, like a Slack channel or a GitHub repository. `lookup` lists the values the connected account allows, with labels you can show in a picker: ```python slack = noclick.Connection.from_name("slack", user="user_42") options = slack.lookup("channel", search="gen") for option in options: print(option.value, option.label) ``` ```ts import { Connection } from "noclick"; const slack = Connection.fromName("slack", { user: "user_42" }); const { options } = await slack.lookup("channel", { search: "gen" }); // and nextPageToken, to pass back as pageToken for (const option of options) { console.log(option.value, option.label); } ``` Pass `options.next_page_token` back as `page_token=` for more. Some lookups depend on other arguments (a sheet inside a spreadsheet); pass those as `arguments`. ## Policy and approvals Your [policy](https://www.noclick.com/docs/guide/policy) can forbid an operation (`noclick.PolicyDenied`) or require a person to approve it first (`noclick.ApprovalRequired`). Rules name the integration's operation (`gmail.send_email_message`), whichever connection runs it, or none, on the user's account or on NoClick's key. A connected account can also carry approval rules of its own, which your account's owner sets in the NoClick app. Once the call is approved, repeat it with the same arguments and it runs once. For a policy approval, pass the error's `approval_id` on the retry; [Policy and approvals](https://www.noclick.com/docs/guide/policy) shows the whole flow. ## Letting an LLM run operations To hand operations to a model as tools, with results the model can read and no exceptions to catch, list them in a [toolkit](https://www.noclick.com/docs/guide/toolkits), or in a hosted [agent's tools](https://www.noclick.com/docs/guide/agents). The refs are the ones `noclick.run` takes: give the model `"exa.search"` beside `"gmail"` and it can search with Exa, with no connection and nothing for the user to connect. The `search` and `fetch` capabilities stay the short way to give a model the web. `exa` gives it Exa's own operations, and moves to your key when you [bring one](https://www.noclick.com/docs/guide/integrations#bring-your-own-key). Every method is in the reference: [`noclick.run`](https://www.noclick.com/docs/reference/python/Connection#run), [`Connection.integrations`](https://www.noclick.com/docs/reference/python/Connection#Connection.integrations), [`Connection.operations`](https://www.noclick.com/docs/reference/python/Connection#Connection.operations), [`Connection.operation`](https://www.noclick.com/docs/reference/python/Connection#Connection.operation), [`Connection.run`](https://www.noclick.com/docs/reference/python/Connection#Connection.run) and [`Connection.lookup`](https://www.noclick.com/docs/reference/python/Connection#Connection.lookup), with the records they return, [`IntegrationInfo`](https://www.noclick.com/docs/reference/python/Connection#IntegrationInfo) and [`IntegrationOperation`](https://www.noclick.com/docs/reference/python/Connection#IntegrationOperation). --- # Agents Source: https://www.noclick.com/docs/guide/agents Create a hosted agent in one call, give it your users' connections, and message it. A hosted **agent** is a model with instructions and tools that NoClick runs for you. You message it, it works (calling tools, acting in your user's accounts), and it replies. Conversation history, tool execution, retries and billing are handled on NoClick's side. ## Create an agent ```python import noclick with noclick.as_user("user_42"): gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True) stripe = noclick.Connection.from_name("stripe", integration="stripe", create_if_missing=True) support = noclick.Agent.from_name( "support", create_if_missing=True, instructions="You answer the user's billing questions. Look invoices up before answering.", tools=[gmail, stripe.only("list_invoices", "search_charges"), "search"], budget_usd_per_month=50, ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; await asUser("user_42", async () => { const gmail = Connection.fromName("gmail", { integration: "gmail", createIfMissing: true }); const stripe = Connection.fromName("stripe", { integration: "stripe", createIfMissing: true }); const support = Agent.fromName("support", { createIfMissing: true, instructions: "You answer the user's billing questions. Look invoices up before answering.", tools: [gmail, stripe.only("list_invoices", "search_charges"), "search"], budgetUsdPerMonth: 50, }); }); ``` `from_name` finds the agent by name and, with `create_if_missing=True`, creates it on first use. `noclick.Agent.create` always creates, and raises `noclick.Conflict` if the name is taken. Names are unique for each user, and for your account. - `instructions` is the system prompt, up to 32,000 characters. - `harness` is what runs it: NoClick's own agent loop (`sdk`, the default) or a coding agent such as Claude Code. See [Harnesses](https://www.noclick.com/docs/guide/agents#harnesses). - `model` is the harness's own model. On `sdk` it's an OpenRouter model id with an `openrouter/` prefix, like `openrouter/anthropic/claude-sonnet-4.5`. Leave it out for the harness's default. - `tools` are [connections](https://www.noclick.com/docs/guide/connections) (a `Connection`, or its name: every operation), some operations of one (`stripe.only("list_invoices")`, or the string `"stripe.list_invoices"`), operations that need no account (`"exa.search"`), the capabilities `"search"` and `"fetch"`, a [toolkit](https://www.noclick.com/docs/guide/toolkits), or NoClick itself (`"noclick"`, see [Give it NoClick itself](https://www.noclick.com/docs/guide/agents#give-it-noclick-itself)). A lazy `create_if_missing` connection is created before the agent. - `budget_usd_per_month` caps what the agent may spend in a month. - `ttl_s` makes it temporary: that many seconds after it's created it expires, taking no more turns, and it's deleted 30 days later. See [Find your agents](https://www.noclick.com/docs/guide/agents#find-your-agents). - `can_request` lets it ask the user for more connections as it goes: see [Asking for connections](https://www.noclick.com/docs/guide/grants). A name in `tools` is a connection in the agent's scope. When no connection has that name, it's the integration of that name, limited to its operations that need no account. So `"exa.search"` gives any agent Exa's web search on NoClick's key, a user's agent included, with nothing to create or connect. `"exa"` gives it every Exa operation that needs no account. A connection named `exa` holding your own key takes over the same names: see [Operations on NoClick's key](https://www.noclick.com/docs/guide/integrations#operations-on-noclick-s-key). A connection must exist when the agent names an operation that needs an account. Without one, creating the agent raises `noclick.InvalidRequest`, which tells you to create it. A lazy `Connection` in `tools`, like `stripe.only(...)` above, is created first; a name in a string is not. An agent holds at most 64 tools. A small integration fits whole (Gmail has 28 operations), but a large one doesn't: Stripe alone has more than 400, so name the operations the agent needs. Every agent can also run shell commands in a sandbox of its own, whether or not you list `code`. To take that away, deny `code` in your [policy](https://www.noclick.com/docs/guide/policy), or create the agent with `shell=False`: see [What it can reach](https://www.noclick.com/docs/guide/agents#what-it-can-reach). ## Harnesses `harness=` picks what runs the agent. The default, `sdk`, is NoClick's own agent loop. The others are coding agents, each running in a sandbox of its own with its own shell and files, thinking with a model provider's key or plan that you hold in a [connection](https://www.noclick.com/docs/guide/connections#model-providers): the agent's `model_connection`. | `harness=` | What runs it | `model=` | `model_connection` | | --- | --- | --- | --- | | `sdk` | NoClick's agent loop | An `openrouter/` id; with a connection, `anthropic/...` or `openai/...` on your key | Optional | | `claude_code` | Claude Code | An alias (`sonnet`, `opus`, `haiku`) or a `claude-...` id | `anthropic` | | `codex` | OpenAI's Codex | A Codex model, like `gpt-5.6-luna` or `gpt-5-codex` | `openai` | | `opencode` | OpenCode | `provider/model`, like `opencode/big-pickle` | The model's provider | | `hermes` | Hermes Agent | `provider/model`, a model that calls tools | The model's provider | | `openclaw` | OpenClaw | `provider/model` | The model's provider | `noclick.Agent.harnesses()` lists them with the version NoClick runs, each one's default model and the providers whose connection it can think with. A harness's [`models()`](https://www.noclick.com/docs/reference/python/Agent#HarnessInfo.models) searches the models it offers, by id or name; leave `model` out for the harness's default. ```python opencode = noclick.Agent.harness_info("opencode") print([m.id for m in opencode.models(search="claude")]) # ['opencode/claude-haiku-4-5', ...] ``` ```ts import { Agent } from "noclick"; const opencode = await Agent.harnessInfo("opencode"); console.log((await opencode.models({ search: "claude" })).map((m) => m.id)); // ['opencode/claude-haiku-4-5', ...] ``` `noclick.Agent.harness_info(name)` is one of them by name; an unknown name raises `noclick.NotFound`, naming the ones there are. An agent on Claude Code, with the Anthropic connection it thinks with: ```python claude = noclick.Connection.from_name("claude", integration="anthropic", create_if_missing=True) coder = noclick.Agent.from_name( "coder", create_if_missing=True, harness="claude_code", model="sonnet", model_connection=claude, instructions="You maintain our docs repo.", tools=["github", "search"], ) if not claude.connected: print(claude.connect_url) # where you paste an Anthropic key reply = coder.message("Fix the broken links in README.md", conversation="docs-1") ``` ```ts import { Agent, Connection } from "noclick"; const claude = await Connection.fromName("claude", { integration: "anthropic", createIfMissing: true }).hydrate(); const coder = Agent.fromName("coder", { createIfMissing: true, harness: "claude_code", model: "sonnet", modelConnection: claude, instructions: "You maintain our docs repo.", tools: ["github", "search"], }); if (!claude.connected) { console.log(claude.connectUrl); // where you paste an Anthropic key } const reply = await coder.message("Fix the broken links in README.md", { conversation: "docs-1" }); ``` Every harness but `sdk` needs a `model_connection`, a connection in the agent's own scope. It may still be pending when you create the agent: until someone connects it, the agent's turns fail with `noclick.NeedsConnection`, which carries the link (a user messaging an account agent gets `noclick.NotAvailable`, with no link). Its link takes a key, or a Claude or ChatGPT plan signed in by the person it belongs to: a Claude plan runs only `claude_code`, a ChatGPT plan `codex`, `opencode` and `hermes` ([model providers](https://www.noclick.com/docs/guide/connections#model-providers)). An account agent thinks with your account's connection, so it can run on your own plan; inside `as_user`, the agent and its connection are the user's, and the user can connect their own plan. A plan suits an agent working for the person whose plan it is; [when a key fits better](https://www.noclick.com/docs/guide/connections#when-a-plan-fits). What changes on a harness other than `sdk`: - **Its sandbox starts first.** A conversation's first turn starts the sandbox, which takes longer than a turn on `sdk`, and it stays warm for about a minute after each reply. While it starts, the turn's operation has `step` `"delivering"`, then `"working"`. - **The reply arrives whole.** The harness doesn't stream its reply as it's written; the whole text comes when the turn ends. - **Instructions set a conversation's start.** A conversation takes the agent's instructions at its first turn, so new instructions apply to conversations that start after the change. - **It has its own shell, file editing and web.** Beside the tools you give it, it runs the harness's built-in commands inside its sandbox. The reply's `tool_calls` list them with `source: "harness"`. Your [policy](https://www.noclick.com/docs/guide/policy) judges the tools you give it, not these. - **Its sandbox time is billed.** The model's tokens go to the connection's key or plan; NoClick bills the sandbox's running time: see [Spend](https://www.noclick.com/docs/guide/spend). - **It doesn't take phone calls yet.** A [phone number](https://www.noclick.com/docs/guide/phone-numbers) answers with an `sdk` agent. Changing `harness` moves each conversation to the new harness at its next turn, history included. A new harness without a `model` takes that harness's default model. ### Your account's key for a user's agent > [!NOTE] > Invite-only beta. [Request access](mailto:support@noclick.com?subject=Invite-only%20beta%20access) to use this section's API; without access, a call answers 404. A user's agent can think with your account's model key instead of one of its own: pass your account's `Connection` object as `model_connection`, as you would to [share a connection](https://www.noclick.com/docs/guide/connections#share-one-across-users). The same rule applies: only a key acting for the whole account may set it, and it must hold a key, since a person's Claude or ChatGPT plan is never shared. ```python claude = noclick.Connection.from_name("claude", integration="anthropic", create_if_missing=True) # your account's key with noclick.as_user("user_42"): coder = noclick.Agent.from_name( "coder", create_if_missing=True, harness="claude_code", model_connection=claude, instructions="You maintain the user's docs site.", ) ``` ```ts import { Agent, Connection, asUser } from "noclick"; const claude = Connection.fromName("claude", { integration: "anthropic", createIfMissing: true }); // your account's key await asUser("user_42", async () => { const coder = Agent.fromName("coder", { createIfMissing: true, harness: "claude_code", modelConnection: claude, instructions: "You maintain the user's docs site.", }); }); ``` ## Where it runs `region=` picks the [region](https://www.noclick.com/docs/guide/regions) its sandboxes run in, for the agent's whole life: a coding harness runs there whole, and an `sdk` agent runs its shell there while its agent loop runs in NoClick's own service. Without it, a child takes its parent's region, else your account's default, else NoClick runs it wherever it has capacity (`region` is `None`). Put it beside its data with `region=db.region`. ```python reviewer = noclick.Agent.from_name( "reviewer", create_if_missing=True, instructions="You review pull requests for our EU customers.", harness="codex", model_connection="openai", region="eu", ) ``` ```ts import { Agent } from "noclick"; const reviewer = Agent.fromName("reviewer", { createIfMissing: true, instructions: "You review pull requests for our EU customers.", harness: "codex", modelConnection: "openai", region: "eu", }); ``` An agent that should always be up, and answer the moment a message arrives, can run on a [machine](https://www.noclick.com/docs/guide/machines) instead: `machine=` puts its harness on an always-on machine at a flat monthly price, with no sandbox per conversation. ## Your code is the definition With `create_if_missing=True`, `from_name` also keeps an existing agent in step with your code: when its instructions, harness, model, `model_connection`, tools, `can_request` or budget differ from what you pass, it's updated to match on first use. Deploy new instructions and every user's agent picks them up the next time your code touches it. Connections a user [granted](https://www.noclick.com/docs/guide/grants) the agent at run time aren't part of the definition, so a deploy never removes them. A field you leave out is never changed, and `None` clears `model_connection` or the budget: moving an agent back to `sdk` is `harness="sdk", model_connection=None`. An agent's `region` never changes: asking for another raises `noclick.Conflict`. ## Message it ```python reply = support.message("Why was I charged twice in March?") print(reply.text) ``` ```ts const reply = await support.message("Why was I charged twice in March?"); console.log(reply.text); ``` `message` sends the text and waits for the reply, an `noclick.AgentReply` with the reply `text`, the `conversation` it belongs to, and the `tool_calls` it made, each `{tool, arguments, result, is_error, created_at}` as [`history()`](https://www.noclick.com/docs/guide/agents#read-conversations) shows them. A message is at most 16,000 characters. A user's agent always acts for that user, wherever you call it from. To keep talking in the same thread, pass the conversation back: ```python follow_up = support.message("Can you refund one of them?", conversation=reply.conversation) ``` ```ts const followUp = await support.message("Can you refund one of them?", { conversation: reply.conversation }); ``` Without a `conversation`, a message starts a new one. You can also choose the key yourself, for example your own ticket id: any 1 to 128 letters, digits and `. _ : @ + -`. A [channel](https://www.noclick.com/docs/guide/channels)'s conversation (`channel:`) is the one key you can't use: only the channel starts turns in it, under its stop rules, posting their replies to the room, so `message` and `wake` refuse it with `noclick.InvalidRequest`. Post in the channel instead. Email, calls and your owner's texts can land in a conversation you name too, so one person reaches the agent in one thread whichever way they get in touch: `conversation=` on an [inbox](https://www.noclick.com/docs/guide/inboxes#choose-the-conversation), on a [number's](https://www.noclick.com/docs/guide/phone-numbers#choose-the-conversation) `answer_with`, and on [`Human.owner().answer_with`](https://www.noclick.com/docs/guide/humans#answer-the-owner-s-texts-and-calls-with-an-agent). ## Send it files `files` sends documents, images, audio and video with a message, at most 10: ```python analyst = noclick.Agent.from_name("analyst") reply = analyst.message("What stands out this quarter?", files=["q3-report.pdf", "chart.png"]) ``` ```ts import { readFile } from "node:fs/promises"; import { Agent } from "noclick"; // A string in `files` is an attachment's id, not a path: upload a local file first. const analyst = Agent.fromName("analyst"); const report = await analyst.upload(await readFile("q3-report.pdf"), { name: "q3-report.pdf" }); const chart = await analyst.upload(await readFile("chart.png"), { name: "chart.png" }); const reply = await analyst.message("What stands out this quarter?", { files: [report, chart] }); ``` A path is uploaded first. To send bytes, a file object or a recorded voice clip, `upload` it and send the attachment: ```python analyst = noclick.Agent.from_name("analyst") clip = analyst.upload(recording, name="question.webm", voice=True) reply = analyst.message("Here's my question.", files=[clip]) ``` ```ts import { Agent } from "noclick"; const analyst = Agent.fromName("analyst"); const clip = await analyst.upload(recording, { name: "question.webm", voice: true }); const reply = await analyst.message("Here's my question.", { files: [clip] }); ``` The turn reads each file the way the agent reads one from any channel: a document's text, a voice clip's transcript (credit-gated, as reading media always is), an image as an image. Files are kept as your account's files, capped as those are (10 MB an image, 15 MB a document, 20 MB audio or video), and an upload is sent once, to the agent it was uploaded to. `history()` shows each message's `files`, and a [stream](https://www.noclick.com/docs/guide/sharing#stream-a-turn) shows them as `file` events. Over HTTP, `POST /v1/agents/{id}/attachments` takes the file's bytes, and the message sends its id in `attachments`. Once sent, a file is one of your account's files, beside the images and videos you [made](https://www.noclick.com/docs/guide/ai) and your calls' recordings. `noclick.attachments()` lists them, newest first (`query` matches their names), and a message from the account sends any of them again: ```python analyst = noclick.Agent.from_name("analyst") [report] = noclick.attachments(query="q3") reply = analyst.message("What changed since this report?", files=[report]) ``` ```ts import { Agent, attachments } from "noclick"; const analyst = Agent.fromName("analyst"); const [report] = await attachments({ query: "q3" }); const reply = await analyst.message("What changed since this report?", { files: [report] }); ``` They're the account's alone: a user's key can't list them or send them. Over HTTP, `GET /v1/attachments` lists them, and a message sends their ids in `attachments` as it does an upload's. ## Don't wait on the reply `send` starts the turn and returns an [operation](https://www.noclick.com/docs/guide/operations) at once, which suits web servers and job queues: ```python turn = support.send("Summarize this customer's open tickets") save(turn.id) reply = noclick.Operation.from_id(turn.id).wait() # later, anywhere: an AgentReply ``` ```ts import { Operation, type AgentReply } from "noclick"; const turn = await support.send("Summarize this customer's open tickets"); save(turn.id); const reply = await Operation.fromId(turn.id).wait(); // later, anywhere: an AgentReply ``` A conversation takes one turn at a time. A second message to a conversation that's still answering another message waits a few seconds, then fails with `noclick.Conflict`. Behind a turn the agent took on its own (a wake-up's or a trigger's), a message waits for it and runs next, before the agent's next wake-up: its operation is `running` at step `waiting` until then. ## How far a turn goes An `sdk` agent's turn stops before it finishes at the first of its limits: 30 steps (model calls), $1 of model calls or 15 minutes. It then makes one more call, without tools, that says what it did and what's left, and ends on a line naming the limit; `reply.ended_by` is `"steps"`, `"spend"` or `"time"` (`None` when the agent answered on its own). A turn its steps end carries on by itself: a follow-up turn on the same conversation picks the work up, up to 3 in a row, each paid and capped like any turn. One follows even when the closing call already finished the work; with nothing left to do, the follow-up is told to end without a reply. A message to the conversation before it starts takes its place. A turn its spend or time limit ends waits to be asked: send "continue" to pick it up. ```python builder = noclick.Agent.from_name("builder") reply = builder.message("Build and ship a site for the bakery", conversation="bakery") if reply.ended_by in ("spend", "time"): reply = builder.message("continue", conversation="bakery") ``` ```ts import { Agent } from "noclick"; const builder = Agent.fromName("builder"); let reply = await builder.message("Build and ship a site for the bakery", { conversation: "bakery" }); if (reply.endedBy === "spend" || reply.endedBy === "time") { reply = await builder.message("continue", { conversation: "bakery" }); } ``` A call the agent repeats with the same arguments and the same result is a loop: the third time, its result tells the agent so, and from then on that call isn't run again in the turn. A call whose result changes, like checking on an operation until it's done, never counts. A coding harness ends its own turns. ## Give a turn more room `turn_limits` lets an agent that does long jobs go further in one turn, each limit left out at its default (at most 200 steps, $25 and 60 minutes); `None` goes back to the defaults. It's for `sdk` agents: a coding harness refuses it with `noclick.InvalidRequest`. ```python builder = noclick.Agent.create( "builder", instructions="You build and ship the sites you're asked for.", tools=["noclick"], turn_limits={"steps": 100, "usd": 2}, ) ``` ```ts import { Agent } from "noclick"; const builder = await Agent.create("builder", { instructions: "You build and ship the sites you're asked for.", tools: ["noclick"], turnLimits: { steps: 100, usd: 2 }, }); ``` ## Wake it up later A **wake-up** gives one conversation a turn later: at a time, after a while, or when something finishes. The turn's message is your `note`, followed by what woke it. ```python support = noclick.Agent.from_name("support") support.wake("ticket-1841", after=3600, note="Check whether the refund has landed, then tell the customer.", user="user_42") finance = noclick.Human(email="finance@acme.com").approve("Refund $95.00 to Ana Lima?") support.wake("ticket-1841", when=finance, note="Finance answered about the refund. Tell the customer what they decided.", user="user_42") ``` ```ts import { Agent, Human } from "noclick"; const support = Agent.fromName("support"); await support.wake("ticket-1841", { afterS: 3600, note: "Check whether the refund has landed, then tell the customer.", user: "user_42" }); const finance = await new Human({ email: "finance@acme.com" }).approve("Refund $95.00 to Ana Lima?"); await support.wake("ticket-1841", { when: finance, note: "Finance answered about the refund. Tell the customer what they decided.", user: "user_42", }); ``` Give it exactly one of: - `at`: a `datetime` with a timezone, from a minute to a year ahead. - `after`: seconds, or a `timedelta`. - `when`: an [operation](https://www.noclick.com/docs/guide/operations) (a person's answer, a connect link, a turn your code started with `send`, a purchase): it wakes the conversation once the operation finishes, with its outcome. One that has already finished wakes it at once. - `every`: a `noclick.Cron`, for a routine: it wakes the conversation at each of the schedule's times, until you cancel it (below). `when` can also be an [event type](https://www.noclick.com/docs/guide/webhooks#event-types) (`*` matches the rest of a name: `"connection.*"`), with `filters` naming fields its `data` must have: it wakes the conversation the first time a matching event happens after you set it (its turn starts within about half a minute, as any due wake-up's does), and the turn reads the event. A user's conversation hears that user's events; one of your account's own conversations hears every event, your users' included. ```python with noclick.as_user("user_42"): assistant = noclick.Agent.from_name("assistant") assistant.wake("trip-planning", when="connection.connected", filters={"name": "calendar"}, note="The user connected their calendar. Book the flights now.") ``` ```ts import { Agent, asUser } from "noclick"; await asUser("user_42", async () => { const assistant = Agent.fromName("assistant"); await assistant.wake("trip-planning", { when: "connection.connected", filters: { name: "calendar" }, note: "The user connected their calendar. Book the flights now.", }); }); ``` A **routine** is a wake-up that comes back. `every` takes a [`Cron`](https://www.noclick.com/docs/guide/triggers#run-on-a-schedule), read in its `tz`; `tz="user"` is the timezone of the user whose conversation it is, and follows them when they move. Unlike a schedule trigger, which starts a fresh conversation each time, a routine wakes the same conversation, so each turn reads what the last one did: ```python coach = noclick.Agent.from_name("coach") standup = coach.wake("standup", every=noclick.Cron("0 9 * * 1-5", tz="user"), note="Ask how yesterday went and what's blocking today.", user="user_42") standup.update(every=noclick.Cron("30 9 * * 1-5", tz="user")) # half an hour later from now on standup.cancel() ``` ```ts import { Agent, Cron } from "noclick"; const coach = Agent.fromName("coach"); const standup = await coach.wake("standup", { every: new Cron("0 9 * * 1-5", { tz: "user" }), note: "Ask how yesterday went and what's blocking today.", user: "user_42", }); await standup.update({ every: new Cron("30 9 * * 1-5", { tz: "user" }) }); // half an hour later from now on await standup.cancel(); ``` A routine stays `pending` after each turn, its `at` the next time and `fired` counting the turns it started. Times it missed, while the conversation was busy or a turn couldn't run, fold into one turn rather than several. `update` changes a pending wake-up's `note` or its time (`at`, `after` or `every`); a turn about to start for what it said before doesn't start. A wake-up that's been delivered, canceled or failed raises `noclick.Conflict`. `wake` returns a `noclick.Wakeup`, and `wakeups()` lists them. `user=` (or `as_user`) says whose thread the conversation is, as it does for `message`, and `user_filter=noclick.ALL_USERS` lists a shared agent's wake-ups across its users' threads, each with its `user_id`, as `conversations` does: ```python support = noclick.Agent.from_name("support") for wakeup in support.wakeups(conversation="ticket-1841", user="user_42"): print(wakeup.source, wakeup.status, wakeup.at, wakeup.note) if wakeup.status == "pending": wakeup.cancel() for wakeup in support.wakeups(status="pending", user_filter=noclick.ALL_USERS): print(wakeup.user_id, wakeup.conversation, wakeup.at) # cancel() acts in its own thread ``` ```ts import { ALL_USERS, Agent } from "noclick"; const support = Agent.fromName("support"); for (const wakeup of await support.wakeups({ conversation: "ticket-1841", user: "user_42" })) { console.log(wakeup.source, wakeup.status, wakeup.at, wakeup.note); if (wakeup.status === "pending") await wakeup.cancel(); } for (const wakeup of await support.wakeups({ status: "pending", userFilter: ALL_USERS })) { console.log(wakeup.userId, wakeup.conversation, wakeup.at); // cancel() acts in its own thread } ``` A wake-up's `status` is `pending`, `delivered` (its turn started: `operation_id` is that turn), `canceled` or `failed` (`error` says why). Its `source` says who set it: `code` for `agent.wake` in your code, `wake_me` for the agent itself. Every agent has a `wake_me(note, at, after_minutes, operation_id, every, conversation)` tool, so it can come back to something on its own: "check again in an hour", "carry on when this job finishes", or "every Monday at 8, send me the week's numbers". One set on an `operation_id` also wakes it each time that operation stops to wait on a person (a build asking a question), with what it asks, so the agent can say what's needed; it still wakes when the operation ends. An operation's outcome reaches a conversation once: `wake_me` on one that already ended answers with its outcome and sets nothing, and a turn that reads the outcome itself takes the wake-up set on it there. `every` is a cron expression read in the agent's `timezone`, else its user's, else UTC, and so is `at` without an offset: the agent writes 9am there as `2026-10-24T09:00:00`, or as `09:00` alone for the next time it's 9am there, and the times it's told back are given there too. An `at` with an offset means what it says. With it come `list_wakeups`, the wake-ups pending in the conversation, and `cancel_wakeup`, and `wake_me` with a `wakeup_id` changes one (in whichever of its conversations it waits), so the agent keeps its own routines when the user asks it to. It changes and cancels only the ones your code or it set, never NoClick's. Your policy names all three `wake_me`, so `deny=["wake_me"]` keeps an agent from setting its own wake-ups. In a [channel](https://www.noclick.com/docs/guide/channels) the channel decides who takes a turn, so an agent has no `wake_me` there. `conversation` sets a wake-up in another of the agent's own conversations instead of the one it's in, and `list_wakeups` and `cancel_wakeup` take it too, so work nobody is waiting on runs apart from the thread a person reads. An agent that [answers the owner](https://www.noclick.com/docs/guide/humans#answer-the-owner-s-texts-and-calls-with-an-agent) can keep its check-ins in a conversation of its own: nothing it replies there reaches anyone, and when there's news it tells them with `notify`, which joins their thread so their reply finds it. The woken turn knows only the note, so the agent writes everything it needs there. It's always the same agent's conversation in the same thread (the account's, or the same user's), never a channel's, and a conversation with someone outside the agent's scope (a stranger's email, an app's visitor) keeps its wake-ups to itself: ```python owner = noclick.Human.owner() assistant = noclick.Agent.from_name("assistant", create_if_missing=True, tools=["search", owner], instructions=( "Once you finish something for the owner, set one check-in a few hours later with wake_me in your " "conversation 'background'. When it wakes you, tell the owner with human__notify only if there's news.")) owner.answer_with(assistant) for wakeup in assistant.wakeups(conversation="background", status="pending"): print(wakeup.at, wakeup.note) # the check-ins it set while talking to the owner ``` ```ts import { Agent, Human } from "noclick"; const owner = Human.owner(); const assistant = Agent.fromName("assistant", { createIfMissing: true, tools: ["search", owner], instructions: "Once you finish something for the owner, set one check-in a few hours later with wake_me in your " + "conversation 'background'. When it wakes you, tell the owner with human__notify only if there's news.", }); await owner.answerWith(assistant); for (const wakeup of await assistant.wakeups({ conversation: "background", status: "pending" })) { console.log(wakeup.at, wakeup.note); // the check-ins it set while talking to the owner } ``` How a wake-up is delivered: - **On time, give or take half a minute.** A wake-up that comes due starts its turn within about 30 seconds. - **Behind a running turn.** A conversation takes one turn at a time, so a wake-up that comes due while a turn runs waits for it, then runs. It never fails with `Conflict`. - **Once, and durably.** A wake-up survives restarts and deploys, and starts exactly one turn. - **Billed like any turn.** Its turn counts toward the agent's budget, its user's cap and your account's funds. A one-off wake-up whose turn your account can't pay for fails instead of running; a routine misses that time and comes round at its next, its `missed` saying when and why (`credits`, `spend_cap` or `budget`). An agent holds up to 100 pending wake-ups, 20 of them routines; one more raises `noclick.QuotaExceeded`. One conversation's wake-ups start at least a minute apart, and a routine comes round at most once a minute (once every 15 minutes for one the agent sets itself, since each time is a billed turn); a schedule that comes round more often raises `noclick.InvalidRequest`. An agent that keeps waking itself, 10 `wake_me` wake-ups in a row with no message or other turn in between, is refused the next one and told to stop or wait for someone. Deleting an agent cancels its pending wake-ups. NoClick wakes an agent itself too, listed beside yours with their own `source`: `update` (a child it spawned, reporting back: see [Coordinators](https://www.noclick.com/docs/guide/sub-agents#updates-wake-the-parent)), `answer` (a person it [asked something](https://www.noclick.com/docs/guide/humans#give-an-agent-a-person) answered, or didn't in time, or the link it asked its user to connect an account with is done, connected or expired: see [Grants](https://www.noclick.com/docs/guide/grants)) and `approval` (a person decided a call your [policy](https://www.noclick.com/docs/guide/policy) held: see [When a call waits for approval](https://www.noclick.com/docs/guide/agents#when-a-call-waits-for-approval)). None of them is set in a [channel](https://www.noclick.com/docs/guide/channels)'s conversation, whose turns the channel starts. Your [webhooks](https://www.noclick.com/docs/guide/webhooks#event-types) hear `agent.wakeup.delivered` (its turn started), `agent.wakeup.missed` (a routine's time your account couldn't pay for, skipped) and `agent.wakeup.failed` (it couldn't run and won't come again: your account couldn't pay for a one-off, or the agent was gone). ## Whose accounts it acts in An agent's connections belong to the same place the agent does. An agent created inside `as_user` belongs to that user and uses that user's connections. An agent created outside `as_user` belongs to your account and uses your account's connections: a support agent with your company's help desk, say. Those include the accounts you connected in the NoClick app, by their id (`"credential:"` or `"credential:.operation"`): see [Accounts connected in the app](https://www.noclick.com/docs/guide/connections#accounts-connected-in-the-app). Every user can message an account agent, each in conversations of their own, but its tools always act in your account's accounts, never the user's. Look it up outside `as_user` and pass `user=` to `message`: inside `as_user`, `Agent.from_name` finds only the user's own agent by that name, never your account's. A user talking to it never gets a link to connect one of your account's connections: an unconnected one just tells the user that tool isn't available. To act in each user's own accounts, give each user an agent of their own. It's the same code, run inside `as_user`: ```python def assistant_for(user_id: str) -> noclick.Agent: with noclick.as_user(noclick.User.from_id(user_id, create_if_missing=True)): gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True) return noclick.Agent.from_name( "assistant", create_if_missing=True, instructions="You help the user with their email. Be brief.", tools=[gmail], ) ``` ```ts import { Agent, Connection, User, asUser } from "noclick"; function assistantFor(userId: string): Promise { return asUser(User.fromId(userId, { createIfMissing: true }), () => { const gmail = Connection.fromName("gmail", { integration: "gmail", createIfMissing: true }); return Agent.fromName("assistant", { createIfMissing: true, instructions: "You help the user with their email. Be brief.", tools: [gmail], }); }); } ``` When a connection the agent uses has no account yet, its tools that need one answer with the connection's link and the agent passes it on in its reply; its operations on NoClick's key already work. The next message after the user connects works normally. To add a connection from another scope beside its own, such as a company's shared Slack or your account's support mailbox, pass that `Connection` object in `tools`: see [Share one across users](https://www.noclick.com/docs/guide/connections#share-one-across-users). ## Read conversations ```python for convo in support.conversations(limit=10): print(convo.conversation, convo.title, convo.turns, convo.cost_usd) for message in support.history(reply.conversation): print(message.role, message.text) for call in message.tool_calls: print(" ", call["tool"], call["arguments"], call["is_error"]) ``` ```ts for (const convo of await support.conversations({ limit: 10 })) { console.log(convo.conversation, convo.title, convo.turns, convo.costUsd); } for (const message of await support.history(reply.conversation!)) { console.log(message.role, message.text); for (const call of message.toolCalls) { console.log(" ", call.tool, call.arguments, call.is_error); } } ``` `history()` reads the whole thread, oldest first. Over HTTP, `GET /v1/agents/{id}/conversations/{conversation}` with `limit` reads its newest `limit` messages, and passing the answer's `next_cursor` as `before` pages back through older ones. Each message keeps the `files` it carried (a reply, the files its turn made and sent you) and, a reply, the `tool_calls` its turn made, each `{tool, arguments, result, is_error, created_at}` as the turn's `tool_call` and `tool_result` events showed it (a result is its preview, an error's is the error; arguments cut short are `{"_truncated": true}`), and the `reasoning` lines its model sent, each `{text, created_at}`, so a chat UI draws a reloaded thread the way it drew the live one. A `user` message that didn't come through the API says how it came in `via`: `whatsapp` or `imessage` (a text to NoClick's number), `call` (said on a phone call) or `email` (mail to one of its [inboxes](https://www.noclick.com/docs/guide/inboxes)). It keeps what the person wrote; what the agent was told about answering in that app reached the model only. Its reply goes back the same way, so a chat UI can caption both. A `user` message no person sent says what started its turn in `trigger`: `{kind, id, title}`, where `kind` is the [trigger](https://www.noclick.com/docs/guide/triggers)'s (`webhook`, `event`, `cron`, `platform` or `queue`) or `wakeup` for a [wake-up](https://www.noclick.com/docs/guide/agents#wake-it-up-later), `id` is the trigger's or the wake-up's, and `title` is what fired (an event's type, a wake-up's note). A NoClick event's (`platform`) also has its `event`, `{id, type, data}`. Its `text` is what the agent read (the trigger's message and the event), so a chat UI can show the turn by what happened (say, "A run of Daily digest failed", from the event's `data`) and its reply as the agent's own update, rather than as something the user said. ## Add a note to a conversation `note` adds a line to a conversation without starting a turn: something your app wants the thread to show and the agent to know, like a plan change or a record the user just picked. It shows in `history()` and [`watch`](https://www.noclick.com/docs/guide/sharing#follow-a-conversation-live) as a `note` message with its `data`, and the agent reads both when its next turn starts, once. A `key` adds it once, so a retry never adds it twice: ```python support = noclick.Agent.from_name("support") added = support.note("ticket-1841", "Ada upgraded to the Pro plan.", data={"plan": "pro"}, key="ticket-1841:pro") ``` ```ts import { Agent } from "noclick"; const support = Agent.fromName("support"); const added = await support.note("ticket-1841", "Ada upgraded to the Pro plan.", { data: { plan: "pro" }, key: "ticket-1841:pro" }); ``` `note` returns `False` when a note with that key was already there. A note is up to 4,000 characters, its `data` up to 4 KB of JSON. Inside `as_user` it goes to that user's conversation, as a message does. Over HTTP, `POST /v1/agents/{id}/conversations/{conversation}/notes` takes `{text, data, key}`. A conversation's `status` is `active`, `awaiting_human` while a turn waits on a person (a question it asked, a call your policy held, a sign-in its browser needs), or `handed_off` while a person has it: see [Give an agent a person](https://www.noclick.com/docs/guide/humans#give-an-agent-a-person). To follow one as it moves, whoever moves it, see [Follow a conversation live](https://www.noclick.com/docs/guide/sharing#follow-a-conversation-live). ## Watch what it did Every turn is an operation. `operation.tool_calls()` lists the tools the turn called, with their arguments, results and timings, and `support.console_url` opens the agent in the developer console, with each conversation and turn. ## Find your agents Every agent says what it's doing: its `status`, when it was `last_active_at` (`None` if it never ran), how many `running_turns` it has, and its `child_count`, the live agents made under it with `parent_id` (or [spawned](https://www.noclick.com/docs/guide/sub-agents)). `Agent.list` filters on them. Inside `as_user`, an account agent's are `None` (its activity is other users' turns too), and the filters below never match it on them: ```python from datetime import datetime, timedelta, timezone for agent in noclick.Agent.list(status="working", order="last_active"): print(agent.name, agent.running_turns, agent.last_active_at) week = datetime.now(timezone.utc) - timedelta(days=7) quiet_coders = noclick.Agent.list(status="idle", active_since=week, harness="claude_code") leftovers = noclick.Agent.list(temporary=True, status="expired") ``` ```ts import { Agent } from "noclick"; for (const agent of await Agent.list({ status: "working", order: "last_active" })) { console.log(agent.name, agent.runningTurns, agent.lastActiveAt); } const week = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const quietCoders = await Agent.list({ status: "idle", activeSince: week, harness: "claude_code" }); const leftovers = await Agent.list({ temporary: true, status: "expired" }); ``` | `status` | Means | | --- | --- | | `working` | A turn is running, queued or waiting on a person (`running_turns` says how many). | | `idle` | Nothing is running. | | `expired` | It was temporary and its `ttl_s` ran out. It takes no turns, and its pending wake-ups are canceled, but you can still read it, its conversations and its jobs for 30 days, when it's deleted. | A temporary agent used to be deleted the moment its `ttl_s` ran out. It now stays `expired` for those 30 days first, so you can still read what it did. Messaging or changing it raises `noclick.Expired`, and `delete()` removes it before then. Its name is free as soon as it expires: `from_name` with `create_if_missing=True` makes a new agent of that name rather than finding the expired one, and `Agent.list(name=...)` lists both. `paused` isn't a status. [Pausing](https://www.noclick.com/docs/guide/triggers#pause-an-agent) stops an agent's triggers, not its work: a paused agent can be `working` on a message your code sent. Read `agent.paused` for that, and `agent.trigger_kinds` for what wakes it, without a read per agent (`"cron" in agent.trigger_kinds` is an automation). The filters are `status`, `active_since` (active at or after a time), `parent` (an agent's children; `agent.children()` is the same), `temporary` (made with `ttl_s`, or not) and `harness`. `order="last_active"` puts the most recently active first and those that never ran last; the default is the newest first. They're cheap on NoClick's side: `last_active_at` is stamped as turns start and end, running turns and live children are counted from indexes, and `status` follows from those, so a filtered listing costs no more than a plain one. ### What was added to it in the app An agent runs on a workflow of its own (its `workflow_id`), which you can open and change in the NoClick app: a trigger wired in to wake it, a step it uses as a tool. `GET /v1/agents/{id}` names what was added there in `added_in_app`: each step's `id`, `name`, `type`, `operation` and `role` (`trigger`, `tool` or `step`), never what NoClick made for the agent itself. `GET /v1/workflows/{workflow_id}` reads that workflow in full (its `agent_id` names the agent), though it isn't listed among your workflows; it changes and runs as the agent, so changing, running or pausing it through `/v1/workflows` raises `noclick.PermissionDenied`. Inside `as_user`, an account agent has no `added_in_app` and its workflow doesn't read. ### Agents built in the app The agents in your [workflows](https://www.noclick.com/docs/guide/workflows), built in the NoClick app, are your account's agents too. `Agent.list` gives them after the ones made through the API, with `origin` `app` (the others say `api`; `origin=` lists only one kind), and you message them the same way: ```python for agent in noclick.Agent.list(origin="app"): print(agent.name, agent.workflow_name, agent.enabled, agent.app_url) triage = noclick.Agent.list(origin="app", name="Triage")[0] reply = triage.message("Anything urgent in the queue this morning?") print(reply.text) ``` ```ts import { Agent } from "noclick"; for (const agent of await Agent.list({ origin: "app" })) { console.log(agent.name, agent.workflowName, agent.enabled, agent.appUrl); } const [triage] = await Agent.list({ origin: "app", name: "Triage" }); const reply = await triage.message("Anything urgent in the queue this morning?"); console.log(reply.text); ``` An app agent's id is `node::`, and it carries its `workflow_id`, `node_id`, `workflow_name`, whether it's `enabled`, your `access` to its workflow (`owner`, or `editor` or `viewer` for one shared with you) and its `app_url`. A message runs a turn on the agent's own step, as a message from the app does, as an operation (`send`) or waited on (`message`), with files; `conversation=` continues a thread. A reply arrives whole, so `stream()` is refused. What the agent is (its instructions, model, tools, triggers) is changed in the app, where its conversations are read too: beyond reading it and messaging it, the API refuses it with a link to its workflow, and only `name` and `harness` filter the list down to it. A user's scope (`as_user`) never sees them, since they belong to the account, and only a workflow you own or edit takes messages. When one messages you with its `message_coordinator` tool, an `agent.asked_owner` [event](https://www.noclick.com/docs/guide/webhooks#event-types) carries it. ## Tell it the time Every turn starts by telling the agent the current date and time, so "last month", "this Friday" and "yesterday's tickets" mean what they say. The time is in the agent's `timezone` (an IANA name) when it has one, else the acting user's [`timezone`](https://www.noclick.com/docs/guide/users), else UTC. It opens the turn's message as a line like "It is Thursday, 1 October 2026, 09:15 in Europe/London (UTC+01:00).", on every harness and every kind of turn (a message, a trigger, a wake-up, a channel or a call), and stays out of the conversation you read back. ```python digest = noclick.Agent.from_name( "digest", create_if_missing=True, instructions="You summarize last week's support tickets every Monday.", timezone="Europe/London", ) ``` ```ts import { Agent } from "noclick"; const digest = Agent.fromName("digest", { createIfMissing: true, instructions: "You summarize last week's support tickets every Monday.", timezone: "Europe/London", }); ``` A schedule can follow the user too: `noclick.Cron("0 8 * * *", tz="user")` fires at 8:00 wherever the agent's user is ([Triggers](https://www.noclick.com/docs/guide/triggers#run-on-a-schedule)). ## Give it a repository `repos=` clones repositories into the agent's workspace, `/workspace/`, at a conversation's first turn, so a coding agent starts every conversation with the code in front of it: ```python github = noclick.Connection.from_name("github", integration="github", create_if_missing=True) fixer = noclick.Agent.from_name( "fixer", create_if_missing=True, harness="claude_code", model_connection="claude", instructions="You fix failing tests in acme/web. Push your fix to a branch named agent/ and open a pull request.", tools=[github.only("create_pull_request")], repos=[github.checkout("acme/web", push=["agent/*"])], ) ``` ```ts import { Agent, Connection } from "noclick"; const github = Connection.fromName("github", { integration: "github", createIfMissing: true }); const fixer = Agent.fromName("fixer", { createIfMissing: true, harness: "claude_code", modelConnection: "claude", instructions: "You fix failing tests in acme/web. Push your fix to a branch named agent/ and open a pull request.", tools: [github.only("create_pull_request")], repos: [github.checkout("acme/web", { push: ["agent/*"] })], }); ``` `connection.checkout(repository, branch=, path=, push=)` takes a repository a `github` [connection](https://www.noclick.com/docs/guide/connections) of the agent's own scope reaches; an agent takes up to 10, and none without a shell (`shell=False`). `path` names the folder (the repository's name by default) and `branch` the branch it starts on (its default branch). The connection may still be pending when you define the agent; a turn that would clone through it first raises `noclick.NeedsConnection` with its `connect_url`. One of your own [repos](https://www.noclick.com/docs/guide/repos) in the agent's scope is checked out with `repo.checkout(branch=, path=, push=)`, the same way. The agent's `git` reaches those repositories only, through NoClick, with a key made for its sandbox and revoked when the sandbox ends; the connection's own token never enters the sandbox, so the model can't read it. `push` lists the branches it may push to, as globs; without it the checkout is read-only. NoClick judges every push on its way to GitHub and refuses one that touches any other ref (a tag, or `main` unless a glob names it), in git's own words (`! [remote rejected] main -> main (NoClick: this checkout may push only to agent/*)`). Whatever the globs allow, the agent can't force-push or delete the repository's default branch, and before a push rewrites or deletes any other branch NoClick keeps its old head for 30 days: on GitHub in the repository, as `refs/noclick/backups//