Agent

Agents: hosted agents you message by name.

class Agent

A hosted agent. Agent.from_name("host", create_if_missing=True, instructions=...) is lazy; agent.message("hi").text talks to it.

An account-level agent is shared: each user that messages it (via as_user or user=) gets its own conversations. A user's own agent always acts for that user.

The agents built in the NoClick app are listed and messaged here too (origin app, an id like node:<workflow>:<node>): send and message reach them, and what they are is changed in the app.

Attributes
idstr
originstr | None

api for an agent made through the API, app for one built in the NoClick app.

namestr
modelstr | None

The model it runs, as its harness names it (sonnet, openrouter/...).

harnessstr | None

What runs it: sdk, claude_code, codex, opencode, hermes or openclaw.

model_connectionstr | None

The connection whose key or plan it thinks with, by name (None on sdk without one). Hidden, like instructions, from a user's session reading an account agent.

instructionsstr | None
toolslist[Any]

What it can use, as listed: refs, {tool, bind} for an operation with arguments bound and {human, tools} for a person.

can_requestlist[str]

What it may ask its user to connect at run time.

user_idstr | None
workflow_idstr | None
node_idstr | None

An app agent's step in its workflow (None for an API agent).

workflow_namestr | None

An app agent's workflow, by name.

accessstr | None

An app agent's workflow as you hold it: owner, or editor or viewer when it was shared with you (only an owner's or an editor's takes messages).

enabledbool | None

Whether an app agent is switched on; a disabled one takes no messages.

app_urlstr | None

Where an app agent is changed: its workflow in the NoClick app.

parent_idstr | None
ttl_sint | None

The time to live it was created with, in seconds (None if it isn't temporary); expires_at is when it runs out, sooner for a child of a temporary parent.

expires_atAny

When a temporary agent's ttl_s runs out (None if it isn't temporary): it's then expired, taking no turns and readable for 30 days before it's deleted.

budget_usd_per_monthfloat | None
regionstr | None

The region its sandboxes run in (noclick.regions()), fixed when it was created; None: wherever NoClick has capacity.

machine_idstr | None

The Machine its harness runs on (None: a sandbox per conversation; invite-only beta).

created_atAny
browser_profile_idstr | None

The browser profile its browser tool signs in with, of its own scope (invite-only beta).

card_idstr | None

The Card it holds and pays with (None: none of its own; a spawned child may pay with its parent's; invite-only beta).

volumesdict[str, str] | None

The volumes its shell mounts, by mount path: {path: volume id}.

memory_idstr | None

Its own memory (agent.memory) as a Memory id, to attach to other agents.

memorieslist[dict[str, Any]] | None

The memories it reads besides its own, in order: each {id, writes} (None for a user's session reading an account agent).

secret_idslist[str] | None

The secrets in its shell's environment, in order (a later one's key wins).

timezonestr | None

The IANA timezone each turn is told the date and time in (None: the acting user's, else UTC).

reposlist[dict[str, Any]] | None

The repositories cloned into its workspace at a conversation's first turn: each its connection, repository, branch, path and the branch globs it may push to.

networkAny

What its shell and code reach: True (anywhere), False (only what it needs to work) or the hosts it may reach besides, as sent.

shellbool | None

Whether it has a shell, the code capability (False only on sdk).

statusstr | None

working (a turn is queued, running or waiting on a person), idle, or expired (past its ttl_s: it takes no turns and stays readable for 30 days). There's no paused: pausing stops triggers, not work, so a paused agent can be working on a message your code sent; paused says that. Like the activity fields below, None for a user's session reading an account agent, whose activity is other users' turns.

last_active_atAny

When one of its turns last started or ended (None: it never ran, or hidden like status).

running_turnsint | None

How many of its turns are queued, running or waiting on a person, any conversation (hidden like status).

child_countint | None

How many live (not expired) children it has; children() lists them (hidden like status).

job_idstr | None

The task it was spawned with, an agent.task operation (None if spawn didn't make it).

used_bylist[dict[str, Any]]

The apps that bind it: kind (project), id, name and the binding name (None for a user's session reading an account agent).

pausedbool

Whether pause() stopped its triggers (False too for a user reading an account agent).

jobJob | None

The task it was spawned with, as a Job (wait() gives its last reply); None for an agent spawn didn't start.

trigger_kindslist[str]

The kinds of its triggers (cron, webhook, event, platform, queue, email, call), so a listing tells automations (agents with cron) apart without reading each agent's triggers. Empty for a user reading an account agent.

console_urlstr | None

This agent in the developer console (None off the hosted API).

a2a_urlstr

The A2A JSON-RPC endpoint (message/send, tasks/get); callers authenticate with a platform key as a bearer token.

memoryAgentMemory

What it remembers across conversations: list, search, read, save and delete named entries (see AgentMemory).

createclassmethodasync

noclick.Agent.create(
    name, *, instructions, model=None, harness=None, model_connection=None, tools=None,
    ttl_s=None, parent_id=None, budget_usd_per_month=None, can_request=None,
    triggers=None, browser_profile=None, card=None, volumes=None, secrets=None,
    timezone=None, repos=None, network=None, shell=None, region=None, machine=None,
    memories=None, user=..., client=None
) -> Agent

Create an agent (names are unique per account and user; a duplicate raises noclick.Conflict with existing_id).

harness is what runs it: "sdk" (the default), NoClick's own agent loop, or a coding agent in a sandbox of its own with a shell and files: "claude_code", "codex", "opencode", "hermes" or "openclaw" (Agent.harnesses() lists them). model is the harness's own: an OpenRouter id with the openrouter/ prefix on sdk, an alias such as "sonnet" on claude_code, a provider/model on opencode, hermes and openclaw; omitted, the harness's default. model_connection is the connection whose key or plan it thinks with (a Connection of a kind="model" integration such as anthropic, or its name, in the agent's own scope; a lazy create_if_missing one is created first): every CLI harness needs one, and it may still be pending. On sdk it's optional, and lets model name anthropic/... or openai/... on your own key. A Connection object from another scope, such as your account's key for a user's agent, stays that connection (invite-only beta): only a key acting for the whole account may set one, and it must hold a key, since a person's Claude or ChatGPT plan is never shared.

tools are connections (a Connection, or its name: every operation), one operation of one ("gmail.send_email_message"), the capabilities "search" and "fetch", a Toolkit, or a Database (its query, execute and schema, or db.only(...); invite-only beta), or a browser ("browser", or "browser.act" and the like; invite-only beta), or a person (a Human, or human.only(...): tools that notify them, ask them and wait for the answer, or hand them the conversation), or the agents capability ("agents", or noclick.Agents.only(...): tools to spawn, message, ask and keep track of other agents), or NoClick itself ("noclick", or noclick.Platform.only(...): your account's API as tools, a purchase or a deletion waiting for your approval): at most 64 tools in all. A string names a connection in the agent's own user (the account for an account agent, whose "credential:<id>" names an account connected in the NoClick app), else an integration's operations that need no account (on NoClick's key), so tools=["exa.search"] (or "exa", all of them) needs no connection. A Connection object from another scope, another user's or the account's, stays that connection (a key acting for the whole account only; invite-only beta). A lazy create_if_missing connection is created first. Every agent can also run shell commands (the code capability; a policy that denies code refuses them, and shell=False removes it). ttl_s makes it temporary: once that many seconds have passed it's expired, taking no turns (its pending wake-ups are canceled) and readable for 30 days, then deleted; parent_id (an Agent or id) makes it a child of that agent, its budget carved from the parent's, with nothing started (parent.spawn also starts a task); budget_usd_per_month caps its spend.

can_request lets it ask its user to connect accounts at run time (a request_connection tool): integration names, single "integration.operation"s, or "*". What the user connects is a grant (grants()), kept apart from this definition.

triggers wake it without a message: noclick.OnWebhook, OnEvent, OnPlatformEvent, Cron and OnQueue (see triggers()).

timezone (an IANA name) is the zone every turn is told the date and time in; without one, the acting user's timezone, else UTC.

region (noclick.regions()) is where its sandboxes run, for good: a coding harness runs there whole, and an sdk agent's shell does while its loop runs in NoClick's own service. Without one it takes its parent's, else your account's default region, else runs wherever NoClick has capacity. region=db.region keeps an agent beside its data. machine (a Machine or id; invite-only beta) runs its harness on an always-on machine instead: no sandbox per conversation, nothing reaped when it's quiet, no per-minute sandbox charge, and it runs in the machine's region. A coding harness there reaches its model only through NoClick's credential proxy, so its model_connection holds an Anthropic, OpenAI or OpenRouter key (a Claude or ChatGPT plan raises noclick.NotAvailable); a machine runs at most 50 agents.

volumes mounts Volumes (or their ids or names: its user's own, then the account's) read-write in its shell at absolute paths ({"/data": vol}), in every conversation, up to 5; not at /workspace, /root or a system folder. An sdk agent's shell sees each as it was when the turn started and saves its writes when the turn ends; a coding harness's sandbox, from when it starts until it stops.

memories are the Memorys it reads besides its own (agent.memory), in order, up to 10: a Memory, its id or name (its user's own, then the account's), or another agent's memory. Each is read-only to it unless given as memory.writable(); a user's agent reads the account's memories but never writes them, and never another user's. Every turn starts with each entry's name and description, its own memory's first.

repos clones repositories into its workspace, /workspace/<name>, at a conversation's first turn: connection.checkout(...) for one a github connection of its own scope reaches, up to 10 (none with shell=False). Its git reaches those repositories only, through NoClick, with a key made for its sandbox; the connection's token never enters it. A push goes through only to the branches the checkout's push globs name (none: read-only): NoClick judges each push on its way to the git host, because a harness's own git push is no tool call your policy could judge. gh only reads them there; pull requests and issues are the connection's operations, in tools. repo.checkout(...), for a NoClick Repo, is in invite-only beta.

secrets (Secrets, ids or names; up to 20) become environment variables in its shell on every turn, a later secret's key winning a clash (a change reaches its next turn): its user's and the account's, an account agent's the account's only. Its shell takes no model provider's key (OPENAI_API_KEY and the like belong on model_connection): a secret holding one raises noclick.InvalidRequest.

shell=False takes the shell (the code capability) away from an sdk agent, and from the children it spawns; a CLI harness refuses it with noclick.InvalidRequest, since its shell is the harness.

network says what its shell and code may reach: True (the default: anywhere), False, or up to 50 hosts ("pypi.org", or "*.example.com" for a domain and its subdomains), over HTTPS on port 443 only. What it needs to work stays reachable, and False leaves only that: an sdk agent's shell reaches NoClick's git proxy when it has repos (its model and tools run on NoClick); a coding harness reaches NoClick's API, NoClick's proxy for its model on a key and for git, and on a Claude or ChatGPT plan that provider's own hosts. Its tools run on NoClick, outside the limit. Without one it takes its parent's, else anywhere, and the children it spawns take its. Where NoClick can't keep to a limited network (a coding harness on a key other than Anthropic, OpenAI or OpenRouter, say), it raises noclick.NotAvailable rather than widen it.

Invite-only beta: browser_profile (a BrowserProfile or id of its own user's, or your account's for an account agent) signs its browser tool in with that person's logins: each conversation gets a browser of its own for a turn, a step only a person can do holds the turn awaiting_human, and its calls follow policy as browser.*. card (a Card or id of its own scope: a user's agent pays with that user's cards, an account agent with your account's; giving one takes the key's cards scope) is what it pays with: it holds the card (a card has one agent at a time) and gets the tools card__status (what it can spend now) and card__purchase (approve before paying: above the card's threshold the turn waits for a person, and their answer is its next turn), which policy names card.status and card.purchase. It never sees the number, and a turn answering someone outside its scope, or a test run, can't buy anything. A child it spawns pays with the same card only when given "card" (or one of the tools) in its tools.

Parameters
namestrrequired
instructionsstrrequired
modelstr | Nonedefault None
harnessstr | Nonedefault None
model_connectionAnydefault None
toolsSequence[Any] | Nonedefault None
ttl_sint | Nonedefault None
parent_idAnydefault None
budget_usd_per_monthfloat | Nonedefault None
can_requestSequence[Any] | Nonedefault None
triggersSequence[Any] | Nonedefault None
browser_profileAnyinvite-only betadefault None
cardAnyinvite-only betadefault None
volumesMapping[str, Any] | Nonedefault None
secretsSequence[Any] | Nonedefault None
timezonestr | Nonedefault None
reposSequence[Any] | Nonedefault None
networkbool | Sequence[str] | Nonedefault None
shellbool | Nonedefault None
regionstr | Nonedefault None
machineAnyinvite-only betadefault None
memoriesSequence[Any] | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Agent

The new agent, loaded.

Usage

Outside as_user the agent is your account's, shared by every user that messages it:

support = noclick.Agent.create(
    "support",
    instructions="You answer questions about our product. Search the docs before answering.",
    tools=["search", "fetch"],
    budget_usd_per_month=50,
)

Inside as_user it's that user's own, and uses the user's connections:

with noclick.as_user("user_42"):
    gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
    assistant = noclick.Agent.create("assistant", instructions="You help the user with their email.", tools=[gmail])

An operation that needs no account needs no connection, for any agent, a user's included:

with noclick.as_user("user_42"):
    scout = noclick.Agent.create("scout", instructions="You research companies for the user.", tools=["exa.search"])

Run it on Claude Code, thinking with your Anthropic key. Its reply arrives whole, and a conversation's first turn starts its sandbox:

claude = noclick.Connection.from_name("claude", integration="anthropic", create_if_missing=True)
coder = noclick.Agent.create(
    "coder",
    instructions="You maintain our docs repo.",
    harness="claude_code",
    model="sonnet",
    model_connection=claude,
    tools=["github", "search"],
)
if not claude.connected:
    print(claude.connect_url)   # where someone pastes an Anthropic key

If the name may be taken, the conflict names the agent already there:

try:
    support = noclick.Agent.create("support", instructions="You answer questions about our product.")
except noclick.Conflict as exc:
    support = noclick.Agent.get(exc.existing_id)

Give a coding agent a repository to work in, branches of its own to push to, and a clock in your team's timezone:

github = noclick.Connection.from_name("github", integration="github", create_if_missing=True)
fixer = noclick.Agent.create(
    "fixer",
    instructions="You fix failing tests in acme/web, push the fix to a branch and open a pull request.",
    harness="claude_code",
    model_connection="claude",
    timezone="Europe/Berlin",
    tools=[github.only("create_pull_request")],
    repos=[github.checkout("acme/web", push=["agent/*"])],
)

Keep a coding agent's sandbox in Europe, with everything it works on:

reviewer = noclick.Agent.create(
    "reviewer",
    instructions="You review pull requests for our EU customers' code.",
    harness="codex",
    model_connection="openai",
    region="eu",
)

An agent that only answers from its tools, with no shell:

faq = noclick.Agent.create("faq", instructions="You answer from our help center.", tools=["search"], shell=False)

Keep its shell to the package index it installs from, beside its data:

analyst = noclick.Agent.create(
    "analyst",
    instructions="You answer questions about our orders with pandas, from the data in /data.",
    volumes={"/data": noclick.Volume.from_name("orders", create_if_missing=True)},
    network=["pypi.org", "files.pythonhosted.org"],
)

from_nameclassmethod

noclick.Agent.from_name(
    name, *, create_if_missing=False, instructions=None, model=None, harness=None,
    model_connection=..., tools=None, ttl_s=None, parent_id=None,
    budget_usd_per_month=..., can_request=None, triggers=None, browser_profile=...,
    card=..., volumes=None, secrets=None, timezone=..., repos=None, network=None,
    shell=None, region=None, machine=None, memories=None, user=..., client=None
) -> Agent

A lazy reference to the agent called name in the current (or given) user: inside as_user only that user's own agent, never an account agent of the same name (reach one of those outside as_user, then message it for the user). With create_if_missing=True your code is the definition: it's created on first use with the fields Agent.create takes, and an existing agent whose instructions, model, harness, model_connection, tools, can_request, budget, timezone, volumes, memories, secrets, repos, network or shell differ is updated to match (the grants its user gave it are kept; a new harness without a model takes that harness's default model, and its conversations carry on on the new harness). So are its triggers: a trigger your code no longer lists is removed and a new one added, while one that stays keeps its webhook URL; mail and call triggers are never touched. So are its browser_profile and card (invite-only beta). A field you leave out is never changed, and None clears model_connection, budget_usd_per_month, browser_profile, card or timezone, as in update(): moving an agent back to sdk takes harness="sdk", model_connection=None. region is fixed once it's created: an existing agent in another region raises noclick.Conflict naming its region.

Parameters
namestrrequired
create_if_missingbooldefault False
instructionsstr | Nonedefault None
modelstr | Nonedefault None
harnessstr | Nonedefault None
model_connectionAnyomit to leave it unchanged
toolsSequence[Any] | Nonedefault None
ttl_sint | Nonedefault None
parent_idAnydefault None
budget_usd_per_monthAnyomit to leave it unchanged
can_requestSequence[Any] | Nonedefault None
triggersSequence[Any] | Nonedefault None
browser_profileAnyinvite-only betaomit to leave it unchanged
cardAnyinvite-only betaomit to leave it unchanged
volumesMapping[str, Any] | Nonedefault None
secretsSequence[Any] | Nonedefault None
timezoneAnyomit to leave it unchanged
reposSequence[Any] | Nonedefault None
networkbool | Sequence[str] | Nonedefault None
shellbool | Nonedefault None
regionstr | Nonedefault None
machineAnyinvite-only betadefault None
memoriesSequence[Any] | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Agent

An agent that loads (or is created, or brought in step with your definition) the first time it's used. Nothing is sent yet.

Usage

Refer to an agent by name without a request:

support = noclick.Agent.from_name("support")
print(support.model)   # loads it now; noclick.NotFound if there's none

Or make your code the definition. instructions is required then, and a field you leave out is never changed on an agent that already exists:

with noclick.as_user("user_42"):
    assistant = noclick.Agent.from_name(
        "assistant",
        create_if_missing=True,
        instructions="You help the user run their day. Ask for access to an account when you need it.",
        tools=["search"],
        can_request=["google_calendar", "gmail"],
    )
    reply = assistant.message("Put lunch with Bo on my calendar for Friday at noon")

An automation is an agent whose definition includes a schedule:

digest = noclick.Agent.from_name(
    "digest",
    create_if_missing=True,
    instructions="You summarize yesterday's support tickets for the team.",
    tools=["search", "slack.send_message_to_channel"],
    triggers=[noclick.Cron("0 8 * * 1-5", tz="Europe/London", message="Post the overnight summary in #support")],
)

Give its shell a dataset and an API key:

analyst = noclick.Agent.from_name(
    "analyst",
    create_if_missing=True,
    instructions="You answer questions about our orders. The data is in /data; call the warehouse "
                 "API with $WAREHOUSE_TOKEN for stock levels.",
    volumes={"/data": noclick.Volume.from_name("datasets", create_if_missing=True)},
    secrets=["warehouse"],
)

from_idclassmethod

noclick.Agent.from_id(agent_id, *, client=None, user=...) -> Agent

A lazy reference to an agent by NoClick id.

Parameters
agent_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Agent

An agent that loads the first time it's used. Nothing is sent yet.

Usage
agent = noclick.Agent.from_id("7b1e4f2a-3c9d-4e8b-a5f6-0d2c8e9b1a47")
reply = agent.message("Hi")

getclassmethodasync

noclick.Agent.get(agent_id, *, user=..., client=None) -> Agent

Load an agent by NoClick id now; an unknown id raises noclick.NotFound.

Parameters
agent_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Agent

The agent, loaded.

Usage
agent = noclick.Agent.get("7b1e4f2a-3c9d-4e8b-a5f6-0d2c8e9b1a47")
print(agent.name, agent.model, agent.user_id)

listclassmethodasync

noclick.Agent.list(
    *, name=None, status=None, active_since=None, parent=None, temporary=None,
    harness=None, order=None, origin=None, limit=None, user_filter=None, user=...,
    client=None
) -> list[Agent]

The agents the current user can message: inside as_user, that user's own agents and the account's (which the user can message but not change; their user_id is None), so a name may match one of each; outside it, the account's. user_filter lists exactly whose agents instead: a User or your id for one (only theirs), noclick.NO_USER (the account's) or noclick.ALL_USERS.

After the account's API agents come the ones built in the NoClick app, which are the account's too (origin="api" or "app" lists only one kind). Of the filters below, only name and harness apply to them; the others leave them out.

Expired agents are listed for the 30 days they're kept. status keeps the working, idle or expired ones; active_since (an aware datetime) those with a turn since then; parent (an Agent or id) its children; temporary those with (or, False, without) a ttl_s; harness those on one harness; and order="last_active" puts the most recently active first, those that never ran last ("created", the default, the newest first). These reads are cheap: last_active_at is a column stamped as turns start and end, and the counts read partial indexes.

Parameters
namestr | Nonedefault None
statusstr | Nonedefault None
active_sinceAnydefault None
parentAnydefault None
temporarybool | Nonedefault None
harnessstr | Nonedefault None
orderstr | Nonedefault None
originstr | Nonedefault None
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Agent]

Every matching agent, newest first (or most recently active first), fetched page by page.

Usage

The agents the current user can message, then one name across every user:

for agent in noclick.Agent.list():
    print(agent.name, agent.user_id)   # user_id is None for an account agent

assistants = noclick.Agent.list(name="assistant", user_filter=noclick.ALL_USERS)   # every user's, and the account's

What's busy right now, and what ran in the last hour:

from datetime import datetime, timedelta, timezone

for agent in noclick.Agent.list(status="working"):
    print(agent.name, agent.running_turns, agent.last_active_at)
hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
recent = noclick.Agent.list(active_since=hour_ago, order="last_active", temporary=False)

The agents built in the app, and a message to one:

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]
print(triage.message("Anything urgent in the queue this morning?").text)

list_pageclassmethodasync

noclick.Agent.list_page(
    *, cursor=None, limit=None, name=None, status=None, active_since=None, parent=None,
    temporary=None, harness=None, order=None, origin=None, user_filter=None, user=...,
    client=None
) -> Page

One page of agents; pass next_cursor back as cursor. Takes list's filters and order.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
namestr | Nonedefault None
statusstr | Nonedefault None
active_sinceAnydefault None
parentAnydefault None
temporarybool | Nonedefault None
harnessstr | Nonedefault None
orderstr | Nonedefault None
originstr | Nonedefault None
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the agents and whose next_cursor is None on the last page.

Usage
page = noclick.Agent.list_page(limit=50)
while True:
    for agent in page.data:
        print(agent.name)
    if page.next_cursor is None:
        break
    page = noclick.Agent.list_page(limit=50, cursor=page.next_cursor)

harnessesclassmethodasync

noclick.Agent.harnesses(*, client=None) -> list[HarnessInfo]

What can run an agent: sdk, NoClick's own agent loop, and the coding agents that each run in a sandbox of their own (claude_code, codex, opencode, hermes, openclaw). On a CLI harness a reply arrives whole rather than streaming, a conversation's first turn starts its sandbox (it stays warm about a minute between turns), and the sandbox's uptime is billed.

Parameters
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[HarnessInfo]

Each harness's name (what harness= takes) and default_model, whether it needs a model_connection and which model providers' integrations serve it, whether its reply streams (streaming) and whether its shell is built in. models() on one searches the models it offers.

Usage

Pick a harness, then search the models it offers:

for harness in noclick.Agent.harnesses():
    print(harness.name, harness.default_model)
[codex] = [h for h in noclick.Agent.harnesses() if h.name == "codex"]
print([m.id for m in codex.models(search="gpt-5")])

Or find which model providers can run Codex, to make its connection:

[codex] = [h for h in noclick.Agent.harnesses() if h.name == "codex"]
print(codex.integrations, codex.notes)   # ['openai'] [...]

harness_infoclassmethodasync

noclick.Agent.harness_info(name, *, client=None) -> HarnessInfo

One of harnesses() by the name harness= takes; an unknown name raises noclick.NotFound, naming the ones there are.

Parameters
namestrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
HarnessInfo

The harness: its default_model, whether it needs a model_connection, the model providers' integrations that serve it, and models() to search what it offers.

Usage
opencode = noclick.Agent.harness_info("opencode")
print(opencode.default_model, [m.id for m in opencode.models(search="claude", limit=5)])

uploadasync

agent.upload(file, *, name=None, mime_type=None, voice=False, peaks=None) -> Attachment

Store a file (a path, bytes or a binary file object) to send this agent with a message. It's kept as an account file, capped as those are: 10 MB an image (JPEG, PNG, WebP or GIF), 15 MB a document, 20 MB audio or video. voice=True marks a recorded clip (audio), which may carry its waveform peaks. message(files=[...]) uploads paths for you; upload first to send bytes or a file object you name, or a voice clip.

Parameters
fileAnyrequired
namestr | Nonedefault None
mime_typestr | Nonedefault None
voicebooldefault False
peaksSequence[float] | Nonedefault None
Returns
Attachment

The attachment; send it with message(files=[...]). An upload is sent once; sent, it's one of the account's files (noclick.attachments()), which the account sends again.

Usage
support = noclick.Agent.from_name("support")
clip = support.upload(recording, name="question.webm", voice=True)
reply = support.message("Here's my question.", files=[clip])

sendasync

agent.send(text, *, conversation=None, files=(), user=...) -> Operation

Start a turn without waiting; .wait() returns the AgentReply. files go with the message, as message sends them.

Parameters
textstrrequired
conversationstr | Nonedefault None
filesSequence[Any]default ()
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Operation

The turn's operation, started. Keep its id to pick the reply up later, from anywhere.

Usage
support = noclick.Agent.from_name("support")
turn = support.send("Summarize this customer's open tickets")
print(turn.id)                                      # save it; then, later, anywhere:
reply = noclick.Operation.from_id(turn.id).wait()   # an AgentReply
print(reply.text)

messageasync

agent.message(
    text, *, conversation=None, files=(), timeout=None, user=...
) -> AgentReply

Send text and wait for the reply. Pass the reply's conversation back to continue the same thread. A channel's conversation (channel:<its id>) raises noclick.InvalidRequest: only the channel starts turns there, so post in the channel instead. files (at most 100) are paths, which are uploaded first, attachments from upload, or (for the account) its files from noclick.attachments(): the turn reads them as the agent's media digest does, a document's text, a voice clip's transcript, an image as an image.

Parameters
textstrrequired
conversationstr | Nonedefault None
filesSequence[Any]default ()
timeoutfloat | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
AgentReply

The reply: its text, the conversation it belongs to and the tool_calls the turn made.

Usage

Ask, then keep talking in the same thread by passing its conversation back:

support = noclick.Agent.from_name("support")
reply = support.message("Why was I charged twice in March?")
print(reply.text)
follow_up = support.message("Can you refund one of them?", conversation=reply.conversation)

Or name the thread yourself, with your own ticket id, say (1 to 128 letters, digits and . _ : @ + -):

support = noclick.Agent.from_name("support")
reply = support.message("Where's my refund?", conversation="ticket-1841")

A shared account agent talks to each of your users in threads of their own:

support = noclick.Agent.from_name("support")
reply = support.message("Where's my invoice?", user="user_42")

With files:

analyst = noclick.Agent.from_name("analyst")
reply = analyst.message("What stands out this quarter?", files=["q3-report.pdf", "chart.png"])

historyasync

agent.history(conversation, *, user=...) -> list[Message]

The messages of one conversation, oldest first.

Parameters
conversationstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
list[Message]

Its messages, each with its role, text, created_at, the files it carried, how it arrived (via, for a text, a call or an email) and, a reply, the tool_calls its turn made; a note has its data.

Usage
support = noclick.Agent.from_name("support")
for message in support.history("ticket-1841"):
    print(message.role, message.via or "api", message.text)

noteasync

agent.note(conversation, text, *, data=None, key=None, user=...) -> bool

Add a line to a conversation without starting a turn: it shows in history and watch as a note with its data, and the agent reads both when its next turn starts, once.

Args: conversation: The current user's conversation (the account's, outside as_user). text: The line, 1-4,000 characters. data: JSON your app reads back with it (at most 4,096 bytes). key: Adds it once: a second note with the same key adds nothing.

Parameters
conversationstrrequired
textstrrequired
datadict[str, Any] | Nonedefault None
keystr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
bool

True when it was added, False when a note with key already was.

Usage
support = noclick.Agent.from_name("support")
support.note("ticket-1841", "Ada upgraded to the Pro plan.", data={"plan": "pro"}, key="plan:pro")

conversationsasync

agent.conversations(*, limit=None, user_filter=None, user=...) -> list[Conversation]

Its conversations, most recent first (the current user's, or those user_filter covers). user_filter reads across users without the user header, which only the signed-in owner may do on a user's own agent; with a secret key, read such an agent's threads without it.

Parameters
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
list[Conversation]

Every matching conversation (or the first limit), fetched page by page. cost_usd is None for a thread with no recorded turns.

Usage

Read the current user's threads (the account's, outside as_user):

support = noclick.Agent.from_name("support")
for convo in support.conversations(limit=10):
    print(convo.conversation, convo.title, convo.turns, convo.cost_usd)

Or every user's threads with a shared agent, each naming its user_id (None for the account's), and read one:

support = noclick.Agent.from_name("support")
for convo in support.conversations(user_filter=noclick.ALL_USERS, limit=20):
    messages = support.history(convo.conversation, user=convo.user_id)

conversations_pageasync

agent.conversations_page(*, cursor=None, limit=None, user_filter=None, user=...) -> Page

One page of its conversations; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Page

A page whose data holds the conversations and whose next_cursor is None on the last page.

Usage
support = noclick.Agent.from_name("support")
page = support.conversations_page(limit=50)
while True:
    for convo in page.data:
        print(convo.conversation, convo.last_activity)
    if page.next_cursor is None:
        break
    page = support.conversations_page(limit=50, cursor=page.next_cursor)

hand_backasync

agent.hand_back(conversation, *, user=...) -> None

Give a conversation back to the agent after it handed it to a person (its hand_off tool). The person's replies stay in its history, and the agent answers again from the next message. The person can hand it back from their page too.

Parameters
conversationstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Usage
support = noclick.Agent.from_name("support")
for convo in support.conversations(user_filter=noclick.ALL_USERS):
    if convo.status == "handed_off":
        support.hand_back(convo.conversation, user=convo.user_id)

spawnasync

agent.spawn(
    task, *, updates=None, conversation=None, instructions=None, tools=None,
    budget_usd=None, ttl_s=None, model=None, harness=None, model_connection=None,
    name=None, user=...
) -> Agent

Make a child agent and start task (its first message, up to 16,000 characters) at once. child.job is that task: a Job that runs while the child works on it, across as many turns as it takes, and finishes when the child stops with nothing left to continue it.

The child lives where its parent does and runs on the parent's harness, model and model connection unless harness, model or model_connection say otherwise. It gets only tools its parent has: tools names some of them (a narrower only(...) of one of the parent's connections is fine, and "card" gives it the card its parent pays with; anything the parent lacks raises noclick.InvalidRequest), and without tools it has none of them (nor its parent's can_request). Like every agent it has its shell (none when its parent has shell=False) and wake_me, and as a spawned child tell_parent, to reach its parent whenever its instructions say to. A person among the parent's tools passes down only with the tools the parent has of them, and a bound operation only as bound or narrower. instructions are its standing instructions: what its updates should say and when to reach out on its own; NoClick adds a short brief saying it works on one task for its parent. budget_usd is carved from the parent's monthly budget, and ttl_s makes it temporary. name defaults to <parent>-<random>.

updates is only when NoClick makes it report: "done" (always on: the job finished, any outcome), "blocked" (it started waiting on a person), noclick.Before(*tools) (before a matching tool call runs) and noclick.Every(interval) (at least that often while it works). Each update is a wake-up for the parent in conversation, the parent's conversation it belongs to (user= says whose, as for message: your key acting for a user spawns from an account agent in that user's thread, and the child is the account's; a user's own session can't); updates that come while the parent is mid-turn arrive together in its next turn. Without conversation, updates wake nobody and wait on the job (job.updates(), and your webhooks' agent.job.update event). A coordinator spawns the same way with its agents tools, its children's updates waking it in the conversation it spawned from. An agent holds up to 20 live children, and children spawn children up to 3 levels below an agent with no parent (noclick.QuotaExceeded). Agent.create(..., parent_id=) still makes a child without starting anything.

Parameters
taskstrrequired
updatesSequence[Any] | Nonedefault None
conversationstr | Nonedefault None
instructionsstr | Nonedefault None
toolsSequence[Any] | Nonedefault None
budget_usdfloat | Nonedefault None
ttl_sint | Nonedefault None
modelstr | Nonedefault None
harnessstr | Nonedefault None
model_connectionAnydefault None
namestr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Agent

The child, loaded, with parent_id set to this agent and job its running task.

Usage

Hand a coordinator's research to a helper and hear back in the launch conversation:

planner = noclick.Agent.from_name("planner")
researcher = planner.spawn(
    "Find three recent launches in our category and summarize what worked.",
    instructions="Report each launch as you finish it with tell_parent. Cite your sources.",
    tools=["search", "fetch"],
    updates=["blocked", noclick.Every(900)],
    conversation="launch-plan",
    budget_usd=0.50,
    ttl_s=3600,
)
print(researcher.job.status)   # running

From your own code, wait for the task the way you'd wait for a message:

planner = noclick.Agent.from_name("planner")
reviewer = planner.spawn("Review the draft in /workspace/launch.md and list what's missing.")
reply = reviewer.job.wait(timeout=600)
print(reply.text)

Hear before money moves (the call still runs; a policy rule is what holds one):

support = noclick.Agent.from_name("support")
refunder = support.spawn(
    "Refund order 1841 if it's within 30 days, then tell the customer.",
    tools=["stripe.create_refund", "gmail.send_email_message"],
    updates=[noclick.Before("stripe.create_refund")],
    conversation="ticket-1841",
    user="user_42",
)

childrenasync

agent.children(*, status=None, limit=None) -> list[Agent]

Its children, any status (an expired one for its 30 days), newest first: Agent.list(parent=self), read where this agent lives.

Parameters
statusstr | Nonedefault None
limitint | None

Return at most this many.

(default None)
Returns
list[Agent]

Each child with its status, last_active_at, running_turns and, if spawn made it, the job it was started with.

Usage
planner = noclick.Agent.from_name("planner")
for child in planner.children(status="working"):
    print(child.name, child.running_turns, child.last_active_at)

jobsasync

agent.jobs(
    *, status=None, kind=None, agent=None, conversation=None, limit=None
) -> list[Job]

The jobs it started: the tasks of the children it spawned (kind="task") and the messages it sent with its agents tools (kind="message", whose only update is done). status takes operation statuses, or "live" for every unfinished one; agent (an Agent or id) keeps the ones one agent does; conversation the ones started from one of its conversations. Read where this agent lives: an account agent's are every user's.

Parameters
statusstr | Sequence[str] | Nonedefault None
kindstr | Sequence[str] | Nonedefault None
agentAnydefault None
conversationstr | Nonedefault None
limitint | None

Return at most this many.

(default None)
Returns
list[Job]

Every matching job (or the first limit), newest first, each with its status, step and latest_update; wait() on one gives its reply.

Usage
coordinator = noclick.Agent.from_name("coordinator")
for job in coordinator.jobs(status="live"):
    update = job.latest_update
    print(job.agent_id, job.status, job.step, update.text if update else "")

askasync

agent.ask(
    question, *, conversation=None, interject=False, timeout=None, user=...
) -> AgentReply

Ask it something without stopping it. By default its model answers from a snapshot of conversation as it stands: its instructions, the conversation, and what a running turn has done so far, with no tools. The question and answer aren't added to the conversation and the agent never sees them; a running turn carries on untouched. The ask is recorded in its activity as an agent.ask operation and billed like a short turn. On a coding harness the snapshot is what NoClick holds: the conversation up to its last finished turn, plus the NoClick tool calls the running turn has made; the harness's own shell commands and file edits only arrive when its turn ends, and its model answers on its connection's key. An agent that thinks with a Claude or ChatGPT plan raises noclick.NotAvailable: a plan runs only in its harness, and answering in a one-off harness session is in invite-only beta (interject=True works).

interject=True puts the question into the running turn instead (the harness's own steering on a coding harness; before its next model call on sdk): the agent sees it, may change course, and answers in that turn, which stays in the conversation. With no turn running, it's an ordinary message. conversation defaults to the one it last worked in for the acting user (a spawned child's: its task's).

Parameters
questionstrrequired
conversationstr | Nonedefault None
interjectbooldefault False
timeoutfloat | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
AgentReply

The answer as an AgentReply; with interject, the reply the turn it joined ended with.

Usage

Check on a child without interrupting it:

planner = noclick.Agent.from_name("planner")
for child in planner.children(status="working"):
    print(child.name, child.ask("In one line: where are you, and what's left?").text)

Steer a coding agent mid-task:

fixer = noclick.Agent.from_name("fixer")
reply = fixer.ask("Skip the flaky e2e suite; the unit tests are enough.", conversation="issue-1841",
                  interject=True)

a2a_cardasync

agent.a2a_card() -> dict[str, Any]

The agent's A2A agent card (the JSON other agents discover it by).

Returns
dict[str, Any]

The card as a dict: the agent's name, its JSON-RPC url and the skills it offers.

Usage
planner = noclick.Agent.from_name("planner")
card = planner.a2a_card()
print(card["url"], [skill["name"] for skill in card["skills"]])

updateasync

agent.update(
    *, instructions=..., model=..., tools=..., name=..., harness=...,
    model_connection=..., budget_usd_per_month=..., can_request=..., timezone=...,
    repos=..., network=..., shell=..., card=..., machine=..., memories=...
) -> Agent

Change fields. Grants its user gave it are kept. A new harness without a model takes that harness's default model, and each conversation carries on on the new harness, with its own memory and the memories it attaches, from its next turn. memories are as in create ([] detaches them all). model_connection=None removes an sdk agent's own model key. timezone is as in create, and timezone=None goes back to the acting user's zone. repos are as in create, from its next turn (a running sandbox restarts with them; a clone already in a conversation's workspace stays as it is). shell is as in create, and network too, from its next turn (a coding harness's running sandbox restarts with it). card (invite-only beta) gives it a card in place of the one it holds, as in create, and card=None takes its card back. machine (invite-only beta) moves it onto a Machine, or with None back to a sandbox per conversation; each conversation moves between turns and keeps its files.

Parameters
instructionsAnyomit to leave it unchanged
modelAnyomit to leave it unchanged
toolsAnyomit to leave it unchanged
nameAnyomit to leave it unchanged
harnessAnyomit to leave it unchanged
model_connectionAnyomit to leave it unchanged
budget_usd_per_monthAnyomit to leave it unchanged
can_requestAnyomit to leave it unchanged
timezoneAnyomit to leave it unchanged
reposAnyomit to leave it unchanged
networkAnyomit to leave it unchanged
shellAnyomit to leave it unchanged
cardAnyinvite-only betaomit to leave it unchanged
machineAnyinvite-only betaomit to leave it unchanged
memoriesAnyomit to leave it unchanged
Returns
Agent

This agent, updated. A field you leave out is unchanged, and changes apply from its next turn.

Usage

Change what it does and what it can use:

support = noclick.Agent.from_name("support")
support.update(
    instructions="You answer billing questions and can refund a charge.",
    tools=["stripe.list_invoices", "stripe.create_refund", "search"],
)

Move it to Codex, on an OpenAI connection:

openai = noclick.Connection.from_name("openai", integration="openai", create_if_missing=True)
support = noclick.Agent.from_name("support")
support.update(harness="codex", model_connection=openai)
print(support.model)   # codex's default model

Tell it the time in New York:

support = noclick.Agent.from_name("support")
support.update(timezone="America/New_York")

Keep its shell off the open internet:

support = noclick.Agent.from_name("support")
support.update(network=["api.stripe.com"])

Let it read what another agent remembers:

sales = noclick.Agent.from_name("sales")
noclick.Agent.from_name("support").update(memories=[sales.memory])

grantsasync

agent.grants() -> list[AgentGrant]

The connections its user gave it at run time (requested or active), oldest first.

Returns
list[AgentGrant]

Each grant with the connection it names, its integration, its status and the operations it covers (None: every one).

Usage
assistant = noclick.Agent.from_name("assistant", user="user_42")
for grant in assistant.grants():
    print(grant.connection, grant.integration, grant.status, grant.operations)
    # google_calendar  google_calendar  active  None

revoke_grantasync

agent.revoke_grant(grant) -> None

Take back a grant (an AgentGrant or its id): its tools leave the agent; the connection stays the user's.

Parameters
grantAnyrequired
Usage
assistant = noclick.Agent.from_name("assistant", user="user_42")
for grant in assistant.grants():
    if grant.integration == "slack":
        assistant.revoke_grant(grant)

deleteasync

agent.delete() -> TrashItem

Delete the agent and the sub-agents under it: they go to the trash, restorable for 30 days. Its pending wake-ups and jobs end now and stay ended if it's restored.

Returns
TrashItem

Its TrashItem: restore() brings it and its sub-agents back.

Usage
noclick.Agent.from_name("support").delete()

wakeasync

agent.wake(
    conversation, *, note, at=None, after=None, when=None, filters=None, every=None,
    user=...
) -> Wakeup

Have it take a turn later in conversation, told note (up to 4,000 characters) and what woke it. Exactly one of: at (an aware datetime), after (seconds, or a timedelta), when: an Operation (a Job, a HumanRequest, a ConnectLink, ...: it wakes when that settles, with its outcome; one already settled wakes it at once), or a NoClick event type ("connection.connected", "human.answered"; * matches the rest of a name), narrowed by filters, dotted paths into the event's data and the values they must have: the first matching event after it was set wakes it, with the event; or every, a noclick.Cron: a routine that wakes the conversation at each of its times until it's canceled (tz="user" follows the conversation's user). A user's conversation hears that user's events; one of your account's own hears every event, your users' included. user= says whose conversation it is, as for message.

A turn already running in the conversation goes first: the wake-up waits for it, never Conflict. A channel's conversation raises noclick.InvalidRequest, as for message. It survives restarts, starts its turn once, and is billed and capped like any turn; when the turn can't be paid for, the wake-up fails with the reason. A recurring one stays pending, due at its schedule's next time after each turn, so times it missed (held, or late) fold into one turn. Every agent can also wake itself with its wake_me tool, and list, change and cancel its own (list_wakeups, cancel_wakeup). An agent holds up to 100 pending wake-ups and 20 recurring ones (noclick.QuotaExceeded); a time is 1 minute to 365 days ahead, a schedule comes round at most every minute (every 15 for one the agent sets itself), and one conversation's wake-ups are delivered at least a minute apart.

Parameters
conversationstrrequired
notestrrequired
atAnydefault None
afterAnydefault None
whenAnydefault None
filtersdict[str, Any] | Nonedefault None
everyAnydefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Wakeup

The wake-up, pending; update() changes it, cancel() calls it off.

Usage

Pick a conversation up tomorrow morning, Berlin time:

from datetime import datetime
from zoneinfo import ZoneInfo

support = noclick.Agent.from_name("support")
support.wake("ticket-1841", at=datetime(2026, 10, 2, 9, 0, tzinfo=ZoneInfo("Europe/Berlin")),
             note="Check whether the replacement shipped, and tell the customer.")

Carry on once the user has connected their calendar:

with noclick.as_user("user_42"):
    assistant = noclick.Agent.from_name("assistant")
    calendar = noclick.Connection.from_name("calendar", integration="google_calendar", create_if_missing=True)
    link = calendar.connect_link(reason="to book your flights")
    assistant.wake("trip-planning", when=link, note="The calendar is connected: book the flights now.")

Or when the user's connection named gmail connects:

with noclick.as_user("user_42"):
    assistant = noclick.Agent.from_name("assistant")
    assistant.wake("inbox-zero", when="connection.connected", filters={"name": "gmail"},
                   note="Gmail is connected: sort the inbox as we discussed.")

Keep a routine: every weekday at 8, in the user's own timezone:

with noclick.as_user("user_42"):
    assistant = noclick.Agent.from_name("assistant")
    briefing = assistant.wake("briefing", every=noclick.Cron("0 8 * * 1-5", tz="user"),
                              note="Brief me on today's calendar and anything urgent in my inbox.")

wakeupsasync

agent.wakeups(
    conversation=None, *, status=None, limit=None, user=..., user_filter=None
) -> list[Wakeup]

Its wake-ups in the acting user's conversations (or one conversation), or in those of the users user_filter covers: those your code set and those it set itself with wake_me, and NoClick's own: a child's update (update), the link it asked its user to connect an account with being done (answer) and a decided approval (approval). status keeps pending, delivered, canceled or failed ones. user_filter (noclick.ALL_USERS, noclick.NO_USER, a User or your id for one) reads across users without the user header, as conversations does: an account agent's wake-ups in every user's threads.

Parameters
conversationstr | Nonedefault None
statusstr | Sequence[str] | Nonedefault None
limitint | None

Return at most this many.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
Returns
list[Wakeup]

Every matching wake-up (or the first limit), newest first, each with its source, when it's due (at) or what it waits for (when), whose thread it's in (user_id) and, once delivered, the turn it started. cancel() acts in that wake-up's own thread.

Usage

What will wake it in the acting user's threads:

support = noclick.Agent.from_name("support")
for wakeup in support.wakeups(status="pending"):
    print(wakeup.conversation, wakeup.source, wakeup.at or wakeup.when, wakeup.note)

Or what will wake a shared agent in any of its users' threads:

support = noclick.Agent.from_name("support")
for wakeup in support.wakeups(status="pending", user_filter=noclick.ALL_USERS):
    print(wakeup.user_id, wakeup.conversation, wakeup.at, wakeup.note)

triggersasync

agent.triggers() -> list[AgentTrigger]

What wakes it besides a message: its schedules, webhooks, app events, NoClick events and queues, and the inboxes and phone numbers that hand it mail and calls.

Returns
list[AgentTrigger]

Each trigger with its kind, its spec, its status (paused while the agent is), the url a webhook listens at, when a schedule's next_run is and when it last_fired_at. An error says why one can't fire.

Usage
support = noclick.Agent.from_name("support")
for trigger in support.triggers():
    print(trigger.kind, trigger.status, trigger.url or trigger.spec)

add_triggerasync

agent.add_trigger(trigger) -> AgentTrigger

Add a trigger: a noclick.OnWebhook, OnEvent, OnPlatformEvent, Cron or OnQueue. An agent holds up to 20. (An agent defined with from_name(create_if_missing=True, triggers=...) gets its list from your code instead, and loses one added here the next time that code runs.)

Parameters
triggerAnyrequired
Returns
AgentTrigger

The trigger as added; a webhook's url is where to send its POSTs. An event trigger whose connection isn't connected yet is error until it is.

Usage

Give an agent a URL your systems can wake it at:

fulfilment = noclick.Agent.from_name("fulfilment")
orders = fulfilment.add_trigger(noclick.OnWebhook(name="orders"))
print(orders.url)

Or wake it on each new HubSpot contact:

sales = noclick.Agent.from_name("sales")
sales.add_trigger(noclick.OnEvent("hubspot", "contact_created", message="Research this contact."))

Or put it on a schedule, which makes it an automation:

support = noclick.Agent.from_name("support")
support.add_trigger(noclick.Cron("0 17 * * 5", tz="America/New_York", message="Send the weekly ticket report"))

remove_triggerasync

agent.remove_trigger(trigger) -> None

Remove a trigger: an AgentTrigger (or its id), or the definition you added (noclick.NotFound if the agent has none like it): an OnWebhook by its name alone, as names are unique on an agent. Mail and call triggers are changed on their inbox or number, not here.

Parameters
triggerAnyrequired
Usage
support = noclick.Agent.from_name("support")
for trigger in support.triggers():
    if trigger.kind == "webhook":
        support.remove_trigger(trigger)

inboxesasync

agent.inboxes() -> list[Inbox]

The inboxes whose mail wakes it (created or updated with agent= it).

Returns
list[Inbox]

Every such inbox, newest first, each with its address.

Usage
support = noclick.Agent.from_name("support")
print([inbox.address for inbox in support.inboxes()])

pauseasync

agent.pause() -> Agent

Stop every trigger from waking it: scheduled runs are skipped (not made up later), webhook calls are refused with 409 so their sender can retry, and app events, mail and calls that arrive meanwhile don't wake it. Messages your code sends still run.

Returns
Agent

This agent, with paused true.

Usage
digest = noclick.Agent.from_name("digest")
digest.pause()
print(digest.paused)   # True

resumeasync

agent.resume() -> Agent

Let its triggers wake it again, including one that stopped after failing the same way five times; a schedule picks up at its next run.

Returns
Agent

This agent, with paused false.

Usage
digest = noclick.Agent.from_name("digest")
digest.resume()

workspace

agent.workspace(conversation, *, user=...) -> AgentWorkspace

The files it works on in one conversation (AgentWorkspace). Sends nothing yet.

Parameters
conversationstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
AgentWorkspace

The conversation's workspace, whose files, read, write and url reach its files.

Usage
support = noclick.Agent.from_name("support")
workspace = support.workspace(conversation="ticket-1841", user="user_42")
for file in workspace.files():
    print(file.path, file.size_bytes)

shareasync

agent.share() -> ShareLink

Turn on its public chat page, or read it if it's on: anyone with the link can talk to the agent, no sign-in, each visitor in conversations of their own. Visitors' turns bill to you and count against its budget, and each visitor is rate limited.

Returns
ShareLink

The link: its url, and disable() and rotate() to turn it off or change it.

Usage
support = noclick.Agent.from_name("support")
link = support.share()
print(link.url)   # https://noclick.com/a/...

streamiterator

agent.stream(text, *, conversation=None, files=(), user=...) -> Iterator[AgentEvent]

Send text (with any files, as message sends them) and watch the turn as it happens: the reply as it's written, each tool call and its result, each file, approval and long action, then the whole reply. The same turn as message, billed the same.

Parameters
textstrrequired
conversationstr | Nonedefault None
filesSequence[Any]default ()
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Iterator[AgentEvent]

AgentEvent values in order, ending with one whose type is done and whose reply is the AgentReply. A turn that fails raises its error instead.

Usage

Print the reply as it's written:

support = noclick.Agent.from_name("support")
for event in support.stream("Where's my refund?", conversation="ticket-1841"):
    if event.type == "text":
        print(event.text, end="", flush=True)
    elif event.type == "tool_call":
        print(f"\n[{event.tool}]")

In async code, relay it to your own UI:

support = noclick.Agent.from_name("support")
async for event in support.stream.aio("Where's my refund?", user="user_42"):
    await websocket.send_json(event.raw)

watchiterator

agent.watch(conversation, *, user=...) -> Iterator[AgentEvent]

Follow one conversation live, whoever moves it: turns a trigger, an answered ask or an approved held call started, messages sent from another client or session, and a person's replies after a hand-off. Your own message and stream turns show up too; tell them apart by operation_id. It runs until you stop reading (break) or the conversation is deleted.

Parameters
conversationstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Iterator[AgentEvent]

AgentEvent values as they happen: message (a whole message, with its role: user, assistant or person, and its files), text as a reply is written, tool_call (held names the approval when policy holds it), tool_result, file, approval and operation (from its "noclick" tools), and status when the conversation starts waiting on a person, is handed off or becomes active.

Usage

Push what happens in a support thread to your own UI:

support = noclick.Agent.from_name("support")
for event in support.watch("ticket-1841", user="user_42"):
    if event.type == "message":
        print(event.role, event.text)
    elif event.type == "tool_call" and event.held:
        print("Waiting for approval:", event.held["url"])

In async code:

support = noclick.Agent.from_name("support")
async for event in support.watch.aio("ticket-1841"):
    await websocket.send_json(event.raw)

rehearseasync

agent.rehearse(message, *, scenario=None, user=...) -> Operation

A test run: a real turn of this agent (its model, instructions and tools) where a simulated world answers every tool call, so nothing is sent, no account is touched and its connections needn't be connected yet (its model connection must be). scenario says what that world is like; the world keeps to it, and without one it's an ordinary busy day. The run is billed like a turn, counts toward the agent's budget, and is no turn in any of its conversations. An account has up to 10 runs queued or running at once (noclick.QuotaExceeded past that).

Parameters
messagestrrequired
scenariostr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Operation

The run's operation (kind agent.rehearse); .wait() gives the Rehearsal: the reply, and the tool_calls it made with what the world answered.

Usage
support = noclick.Agent.from_name("support")
run = support.rehearse(
    "Order 1841 arrived broken. I want my money back.",
    scenario="Order 1841 was delivered on Sep 2 and paid by card; the customer has asked once before.",
).wait()
print(run.reply)
for call in run.tool_calls:
    print(call["tool"], call["arguments"])

AgentGrant

class AgentGrant

A connection the agent's user gave it at run time (it asked with its request_connection tool, within can_request). status is requested until the user connects, then active; operations None means every operation.

Attributes
idstr
connectionstr
integrationstr
operationslist[str] | None
statusstr
reasonstr | None
conversationstr | None
created_atAny
granted_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

AgentReply

class AgentReply

The agent's answer to one message.

Attributes
textstr
conversationstr | None
tool_callslist[dict[str, Any]]

The turn's tool calls, each {tool, arguments, result, is_error, created_at} as history() shows a reply's (a failed call's result is its error); a coding harness's own steps add source: "harness".

operation_idstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

model_usageModelUsage | None

The turn's model calls on your own key or plan (None on NoClick's key, whose cost is its cost_usd).

Conversation

class Conversation

One thread with an agent; pass conversation back to continue it or to history().

Attributes
conversationstr
user_idstr | None
titlestr | None
previewstr | None
turnsint | None
cost_usdfloat | None
last_activityAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

statusstr | None

active; awaiting_human while a turn waits on a person (an ask, a held call, a sign-in); handed_off while a person has it.

handed_off_tostr | None

Who a handed_off conversation is with: the person's email or phone.

model_usageModelUsage | None

Its model calls on your own key or plan, all turns together (None on NoClick's key).

Message

class Message

One message of a conversation: its role is user, assistant, person (someone it was handed off to) or note (a line added with agent.note, with its data).

Attributes
rolestr
textstr
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

fileslist[Attachment]

The files it carried (a user message's, sent with files=).

tool_callslist[dict[str, Any]]

A reply's tool calls: each {tool, arguments, result, is_error, created_at}, as its turn's tool_call and tool_result events showed it (arguments cut short are {"_truncated": True}).

viastr | None

How a user message reached the agent when not through the API: whatsapp, imessage (a text to NoClick's number), call (said on a phone call) or email (mail to one of its inboxes).

datadict[str, Any] | None

A note's data, as agent.note added it.

triggerdict[str, Any] | None

What started the turn of a user message no person sent: {kind, id, title}, kind a trigger's (webhook, event, cron, platform, queue) or wakeup; a NoClick event's has its event ({id, type, data}). None for a message someone sent.

reasoninglist[dict[str, Any]]

A reply's reasoning: each {text, created_at}, a short line of what its model was thinking, as its turn's reasoning events showed it.

HarnessInfo

class HarnessInfo

What can run an agent: one of Agent.harnesses(). sdk is NoClick's own agent loop; the others are coding agents that run in a sandbox of their own, with a shell and files, and think with the key or plan of a model_connection. models() searches the models it offers.

Attributes
namestr

What harness= takes: sdk, claude_code, codex, opencode, hermes or openclaw.

labelstr | None
versionstr | None

The version NoClick runs (None for sdk).

default_modelstr

The model an agent on it gets when you give none.

custom_modelsbool

Whether model= also takes models models() doesn't offer.

model_connectionstr

"required" for every CLI harness, "optional" for sdk.

integrationslist[str]

The model providers (kind="model" integrations) whose connection it can think with.

streamingstr

"tokens" when the reply streams as it's written, "final" when it arrives whole.

shellstr

"built_in" when it has its own shell and file tools, "code_capability" when its shell is the code tool.

noteslist[str]

What to know before choosing it (a model that needs a particular key, say).

rawdict[str, Any]

The response JSON exactly as the API sent it.

modelsasync

harness_info.models(search=None, *, limit=50) -> list[HarnessModel]

The models it offers, its default first: the first limit (at most 200), or those whose id or label contains search (ignoring case). A search after a few quiet minutes reads the providers' lists afresh, so it can take seconds.

Parameters
searchstr | Nonedefault None
limitint

Return at most this many.

(default 50)
Returns
list[HarnessModel]

Each model's id (what model= takes) and label. A harness whose custom_models is True also runs ids it doesn't list.

Usage

Find a Claude model for an agent on OpenCode:

[opencode] = [h for h in noclick.Agent.harnesses() if h.name == "opencode"]
for model in opencode.models(search="claude", limit=10):
    print(model.id, model.label)

Check that a model is one Claude Code offers before creating the agent:

[claude_code] = [h for h in noclick.Agent.harnesses() if h.name == "claude_code"]
offered = [m.id for m in claude_code.models(search="opus")]
print("opus" in offered)

HarnessModel

class HarnessModel

A model a harness offers, by the id its model= takes.

Attributes
idstr
labelstr | None

ModelUsage

class ModelUsage

What an agent's model calls on your own key or plan (its model_connection) used: tokens in (cached_tokens of them read from the provider's cache), tokens out, and what they come to in US dollars at the provider's list price. NoClick doesn't bill these calls (the key or plan pays); this is how you see them. A coding harness on a plan counts what it reports, priced as if it ran on a key.

Attributes
input_tokensint
output_tokensint
cached_tokensint
usdfloat
rawdict[str, Any]

The response JSON exactly as the API sent it.

AgentMemory

class AgentMemory

agent.memory: named notes the agent keeps across conversations. Every turn starts with each entry's name and description; the agent reads a body when it needs one, and saves what it learns. Your code can seed, read, correct and forget entries the same way.

An account agent keeps two layers: entries saved outside as_user are the account's, read in every user's conversations (your refund policy), and what it learns while talking to a user is that user's alone. A user's own agent has one memory, the user's. The agent changes the account's entries only in conversations your code has with it outside as_user, and only reads its memory when it answers someone outside your account (an inbox's sender, a caller, a visitor to its share link). A long conversation also keeps its checkpoint here, in that conversation's layer (MemoryEntry.conversation).

It's a noclick.Memory (its id is agent.memory_id) that goes with the agent: another agent of its scope can read it too, with memories=[agent.memory], and named memories (noclick.Memory) are attached the same way.

Usage
support = noclick.Agent.from_name("support")
support.memory.save("refunds", "Refund within 30 days of purchase.", description="Our refund rules")
print([entry.name for entry in support.memory.list()])

listasync

agent.memory.list(*, user=...) -> list[MemoryEntry]

The entries of one layer: the account's outside as_user, else the user's.

Parameters
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
list[MemoryEntry]

Each entry's name, description, version and when it last changed, by name; content is None here, so read an entry for its body.

Usage
support = noclick.Agent.from_name("support")
for entry in support.memory.list():
    print(entry.name, entry.description, entry.updated_by)

searchasync

agent.memory.search(query, *, limit=None, user=...) -> list[MemoryEntry]

Search what the agent remembers in a scope, the way the agent does: a user's entries and the account's. An entry matches when its name, description or content holds any of the query's words (a word also finds the longer words it starts: refund finds refunds), or its name or description holds the query; the more words, the better.

Parameters
querystrrequired
limitint | None

Return at most this many.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
list[MemoryEntry]

The best matches first (10 unless limit says otherwise, at most 100), each with its content and user_id (None for the account's entries).

Usage
support = noclick.Agent.from_name("support")
for entry in support.memory.search("refund policy", user="user_42"):
    print(entry.name, entry.content)

readasync

agent.memory.read(name, *, user=...) -> MemoryEntry

One entry in full; a name with no entry raises noclick.NotFound.

Parameters
namestrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
MemoryEntry

The entry with its content and the version to pass back when you change it.

Usage
support = noclick.Agent.from_name("support")
refunds = support.memory.read("refunds")
print(refunds.version, refunds.content)

saveasync

agent.memory.save(
    name, content, *, description=None, version=None, user=...
) -> MemoryEntry

Write an entry, creating it if the name is new. description is the line the agent sees every turn (required for a new entry, kept when you leave it out). With version, the save only lands if the entry is still at that version, else noclick.Conflict; without it, it replaces whatever is there.

Parameters
namestrrequired
contentstrrequired
descriptionstr | Nonedefault None
versionint | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
MemoryEntry

The entry as saved, with its new version.

Usage

Seed what every conversation should know:

support = noclick.Agent.from_name("support")
support.memory.save(
    "refunds", "Refund within 30 days of purchase, to the original card.",
    description="Our refund rules",
)

Correct an entry without losing an edit the agent made meanwhile:

support = noclick.Agent.from_name("support")
entry = support.memory.read("refunds")
try:
    support.memory.save("refunds", entry.content + "\nNo refunds on gift cards.", version=entry.version)
except noclick.Conflict:
    print("The agent changed it since; read it again.")

deleteasync

agent.memory.delete(name, *, version=None, user=...) -> None

Forget an entry. With version, only if it is still at that version (else noclick.Conflict). The agent can save the name again later.

Parameters
namestrrequired
versionint | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Usage
support = noclick.Agent.from_name("support")
entry = support.memory.read("old-pricing")
support.memory.delete("old-pricing", version=entry.version)

entriesasync

agent.memory.entries(*, user=...) -> list[MemoryEntry]

list, as noclick.Memory calls it, so code reads any memory the same way.

Parameters
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
list[MemoryEntry]

Each entry's name, description and version, by name, without its content.

Usage
for memory in (noclick.Agent.from_name("support").memory, noclick.Memory.from_name("company")):
    print([entry.name for entry in memory.entries()])

forgetasync

agent.memory.forget(name, *, version=None, user=...) -> None

delete, as noclick.Memory calls it, so code changes any memory the same way.

Parameters
namestrrequired
versionint | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Usage
support = noclick.Agent.from_name("support")
support.memory.forget("old-pricing")

MemoryEntry

class MemoryEntry

One named note in an agent's memory. version goes up with every save; pass the one you read back to save or delete so you never overwrite a change the agent made since. content is None in a listing (read loads it). updated_by is agent, api or console.

conversation is set on a conversation's checkpoint: a long conversation keeps its older history folded into one entry, named context. and a hash, which its turns read in place of that history. Read it to see what the agent carries forward; an edit wins over the next summary, and deleting it forgets that history for good. The agent never sees it outside that conversation.

Attributes
namestr
descriptionstr
contentstr | None
versionint
updated_atAny
updated_bystr | None
user_idstr | None
conversationstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

AgentWorkspace

class AgentWorkspace

The files an agent works on in one conversation: what it writes with its shell or its tools, and what you put there for it. It lasts as long as the conversation does, and every turn of that conversation sees the same files under /workspace. Each user's thread with an account agent has its own. (A noclick.Workspace is something else: a home for channels and their members.)

Attributes
conversationstr

The conversation whose files these are.

filesasync

workspace.files() -> list[WorkspaceFile]

Every file in the workspace.

Returns
list[WorkspaceFile]

Each file's path (relative to /workspace), size, when it last changed and a download url. An empty list for a conversation whose agent hasn't written anything.

Usage
support = noclick.Agent.from_name("support")
for file in support.workspace(conversation="ticket-1841").files():
    print(file.path, file.size_bytes, file.url)

readasync

workspace.read(path) -> bytes

Read one file; a missing file raises noclick.NotFound.

Parameters
pathstrrequired
Returns
bytes

The file's bytes, exactly as stored; decode text yourself.

Usage
analyst = noclick.Agent.from_name("analyst")
report = analyst.workspace(conversation="q3-review").read("report.csv")
print(report.decode()[:200])

writeasync

workspace.write(path, data) -> WorkspaceFile

Put a file in the workspace (bytes, or text as UTF-8), replacing one at that path, so the agent finds it on its next turn. At most 100 MB. A coding harness's sandbox stays up between turns and sees the file once it starts again (it stops a few minutes after its last turn), so write there before the conversation's first turn or between its sessions.

Parameters
pathstrrequired
databytes | strrequired
Returns
WorkspaceFile

The file as stored, with its download url.

Usage
analyst = noclick.Agent.from_name("analyst")
workspace = analyst.workspace(conversation="q3-review")
workspace.write("sales.csv", "region,total\nnorth,1200\nsouth,950\n")
reply = analyst.message("Chart sales.csv by region.", conversation="q3-review")

urlasync

workspace.url(path, *, expires_in_s=None) -> str

A link that downloads one file without a key, for your user to click.

Parameters
pathstrrequired
expires_in_sint | Nonedefault None
Returns
str

The link; it works for expires_in_s seconds (an hour by default, a day at most).

Usage
analyst = noclick.Agent.from_name("analyst")
link = analyst.workspace(conversation="q3-review").url("chart.png", expires_in_s=600)
print(link)

WorkspaceFile

class WorkspaceFile

A file in an agent's workspace for one conversation (AgentWorkspace). url downloads it without a key for about an hour.

Attributes
pathstr
size_bytesint
modified_atAny
urlstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

AgentEvent

class AgentEvent

One moment of a streamed turn or a watched conversation. type is text (text is the next piece of the reply), tool_call (tool, its wire name like gmail__send_email_message, and its arguments; held when policy holds it for a person), tool_result (tool, its result: the start of what it returned, up to 500 characters, or its error; and whether it is_error), file (a file the message carried, role user, or the turn made, assistant), reasoning (text is one short line of what the model is thinking, the title of a section of its reasoning, sent as it completes), status (compacting while a long conversation's older history is summarized, then compacted), approval (its "noclick" tools held a purchase or a deletion for the account's owner: the approval), operation (they started a long action: the operation, which comes again with its progress each time the work reports some and once more when it ends) or done (reply is the whole AgentReply, as message returns it). agent.watch() also yields message (a whole message: its role, text, files, via or trigger and, a reply, its tool_calls and reasoning; a note, its data) and status (the conversation's new status). operation_id names the turn an event belongs to (None for a message no turn sent, like a person's reply). history() reads messages in the same shape. The type call is reserved for a phone call's moments.

Attributes
typestr
textstr | None
toolstr | None
argumentsdict[str, Any] | None
resultAny
is_errorbool
replyAgentReply | None
rolestr | None

A message's sender: user, assistant, person (someone it was handed off to) or note (a line added with agent.note).

helddict[str, Any] | None

A held tool_call: who approver is and, when the request is addressed to the scope you read as, the approval's request_id and its page url.

statusstr | None

A status event's: the conversation's new status (active, awaiting_human or handed_off), or a turn's own (compacting, then compacted).

operation_idstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

fileAttachment | None

A file event's file.

fileslist[Attachment]

A message event's files.

tool_callslist[dict[str, Any]]

A message event's tool calls (a reply's): each {tool, arguments, result, is_error}, as its turn's tool_call and tool_result events showed it.

approvaldict[str, Any] | None

An approval event's: its id, summary, exact price (a purchase), expires_at and, read as the account, the url of the page where its owner approves.

operationdict[str, Any] | None

An operation event's: the long action's id (an Operation's), kind, status and summary and, once the work reported some, its progress (Operation.progress, with its last 3 lines).

viastr | None

A user message's way in, when not the API: whatsapp, imessage, call or email.

datadict[str, Any] | None

A note message's data.

triggerdict[str, Any] | None

A user message no person sent: what started its turn (Message.trigger).

reasoninglist[dict[str, Any]]

A message event's reasoning (a reply's): each {text, created_at} (Message.reasoning).

Rehearsal

class Rehearsal

A test run: a real turn of the agent (its model, instructions and tools) where a simulated world answers every tool call, so nothing is sent and no account is touched. tool_calls are the calls it made, each with the tool (its tool name, like gmail__search_emails), its arguments and the result the world answered (None if the world couldn't); transcript is the turn step by step: role assistant steps (its text between calls, then the reply) and tool steps. On a coding harness, its own shell and file steps are tool steps with source harness: they ran for real, in the run's own sandbox. cost_usd is what the run cost you.

Attributes
replystr
tool_callslist[dict[str, Any]]
transcriptlist[dict[str, Any]]
scenariostr | None
cost_usdfloat | None
operation_idstr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

Wakeup

class Wakeup

A turn an agent will take later, in one of its conversations: at a time, when an operation settles, on a NoClick event, or at each time of a schedule (agent.wake, or its own wake_me tool). At its moment the conversation takes a turn whose message is the note, followed by what woke it. A turn already running in that conversation goes first: the wake-up waits behind it, one turn at a time. It survives restarts, starts its turn once, and is billed and capped like any turn. A recurring one (every) stays pending after each turn, due at its schedule's next time; times it missed fold into one turn. NoClick sets its own too: for a child's update, the link the agent asked its user to connect an account with being done, and a decided approval of a held call. Your webhooks hear each one delivered or failed (agent.wakeup.delivered, agent.wakeup.failed), and each time a recurring one missed because your account couldn't pay (agent.wakeup.missed).

Attributes
idstr
agent_idstr
user_idstr | None

Whose conversation it wakes (None: the account's).

conversationstr

The conversation that takes the turn.

notestr

What the agent is told when it wakes.

atAny

When it's due (a time wake-up), or when what it waited for happened.

whendict[str, Any] | None

What it waits for besides a time: {operation_id} or {event, filters} (None for a time).

everyCron | None

Its schedule, for a recurring wake-up (None for one that comes once); at is its next time.

firedint

How many turns it has started (a recurring one counts up).

sourcestr

What set it: code (agent.wake), wake_me (the agent's own tool), update (a job's update for this agent as its parent), answer (the link it asked its user to connect an account with is done: connected or expired) or approval (a held call was decided).

statusstr

pending, delivered (its turn started), canceled or failed; a recurring one stays pending until it's canceled.

woken_bydict[str, Any] | None

What woke it: {at} for a time (once delivered), {operation_id, kind, status, result} (or error) once its operation settled, {update} for a job's update, and {event} (the event as Event.list reads it) for an event; None until then.

operation_idstr | None

The turn it started (an agent.message operation).

depthint

How many wake_me wake-ups in a row led to this one, with nothing else in between (0 for any other).

errordict[str, Any] | None

Why it failed: {type, message} (the account couldn't pay, say).

misseddict[str, Any] | None

A recurring one's last missed time: {at, reason}, reason being credits, spend_cap (the account's monthly cap) or budget (the agent's, its key's or its user's); None if it never missed.

created_atAny
delivered_atAny

When its turn started (None until then).

operationOperation | None

The turn it started, as an Operation (None until it's delivered).

cancelasync

wakeup.cancel() -> Wakeup

Call it off before it's due. A wake-up that already started its turn, or was canceled or failed, raises noclick.Conflict.

Returns
Wakeup

This wake-up, canceled.

Usage
digest = noclick.Agent.from_name("digest")
for wakeup in digest.wakeups("weekly-report", status="pending"):
    wakeup.cancel()

updateasync

wakeup.update(*, note=None, at=None, after=None, every=None) -> Wakeup

Change a pending wake-up: its note, and its time with one of at, after and every (a Cron; a one-off time makes a recurring one come once more). A delivery under way of what it said before starts no turn. One waiting on an operation or an event keeps that; one already delivered, canceled or failed raises noclick.Conflict.

Parameters
notestr | Nonedefault None
atAnydefault None
afterAnydefault None
everyAnydefault None
Returns
Wakeup

This wake-up, as it is now.

Usage
digest = noclick.Agent.from_name("digest")
[weekly] = digest.wakeups("weekly-report", status="pending")
weekly.update(every=noclick.Cron("0 8 * * 1", tz="Europe/London"))

Job

class Job(Operation)

Work one agent gave another: a spawned child's task (kind agent.task), or a message a coordinator sent with its agents tools (kind agent.message), whose only update is done. It's an Operation: wait() returns the AgentReply.

A task runs while the child works on it, across as many turns as it takes, and finishes when its conversation goes quiet with nothing left to continue it: no turn running, no wake-up pending, nothing waiting on a person. While it runs, step is working (a turn is running) or waiting (only a wake-up is pending); it's awaiting_human (step blocked) while it waits on a person: a held call, or a connection it asked its user for. It succeeded once the child stopped, its last reply the result (a child that needs a decision says so and stops; the parent's message to it starts a new job in the same conversation), failed when a turn failed, canceled when canceled (its running turn stops and its pending wake-ups are called off), and expired when the child's ttl_s ran out first.

Attributes
started_bystr | None

The agent whose job it is: a task's parent, whether your code spawned it or the parent did.

conversationstr | None

The conversation the worker does it in.

parent_conversationstr | None

The parent's conversation its updates wake (None: they wake nobody).

required_updateslist[Any]

The updates its parent required, as on the wire: done, blocked, {before}, {every_s}.

latest_updateJobUpdate | None

Its newest update (None before the first).

agentAgent

The agent doing the work, lazy.

listclassmethodasync

noclick.Job.list(*, limit=None, **filters) -> list[Job]

Spawned children's tasks across your agents (kind agent.task), newest first. Takes Operation.list's filters except kind (agent= keeps the ones one agent does). agent.jobs() reads the ones one agent started, its message jobs (kind agent.message) too.

Parameters
limitint | None

Return at most this many.

(default None)
**filtersAny
Returns
list[Job]

Every matching job (or the first limit), each with its status, step and latest_update.

Usage
for job in noclick.Job.list(status="awaiting_human", user_filter=noclick.ALL_USERS):
    print(job.agent_id, job.started_by, job.latest_update)

list_pageclassmethodasync

noclick.Job.list_page(*, cursor=None, limit=None, **filters) -> Page

One page of jobs; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
**filtersAny
Returns
Page

A page whose data holds the jobs and whose next_cursor is None on the last page.

Usage
page = noclick.Job.list_page(status="failed", limit=50)
for job in page.data:
    print(job.agent_id, job.error)

summaryclassmethodasync

noclick.Job.summary(**filters) -> OperationSummary

How many jobs match, by status; takes Operation.summary's filters except kind.

Parameters
**filtersAny
Returns
OperationSummary

The number that match in total, and per operation status in by_status.

Usage
counts = noclick.Job.summary(status="live")
print(counts.total, counts.by_status["awaiting_human"])

updatesasync

job.updates() -> list[JobUpdate]

Every update it has sent its parent, oldest first.

Returns
list[JobUpdate]

Each JobUpdate with its kind, the child's text and, by kind, the call it was about to make, what it waits on, or the result.

Usage
planner = noclick.Agent.from_name("planner")
for job in planner.jobs(kind="task"):
    for update in job.updates():
        print(update.created_at, update.kind, update.text)

JobUpdate

class JobUpdate

One update a job sent its parent. kind is done (the job finished, any outcome: result or error), blocked (it started waiting on a person: waiting_on), before (it's about to make the call in tool with arguments), every (it was due one) or message (the child's own tell_parent). text is what the child wrote (for blocked, NoClick's line saying what it waits on); status is the job's status then, and wakeup_id the parent's wake-up it set (None when nothing was there to wake).

Attributes
idstr
job_idstr
agent_idstr
kindstr
textstr | None
toolstr | None
argumentsdict[str, Any] | None
statusstr | None
resultAny
errordict[str, Any] | None
waiting_ondict[str, Any] | None

A blocked update's wait: {kind, url, operation_id}, kind being connection (a link it asked its user to connect an account with: url) or approval (a held call; never a url, so no agent approves its child's call). person and sign_in are in invite-only beta.

wakeup_idstr | None
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

Before

noclick.Before(*tools)

An update the parent requires before its child makes a tool call: Before with tool patterns written as policy writes them ("gmail.send_email_message", "stripe.*", "code"). When the child calls a matching tool, NoClick sends the parent an update holding the call and what the child wrote before it (its reason), then the call runs: the parent hears first but doesn't hold it (a call that must wait for a yes is a policy require_approval rule). A coding harness's own shell commands and file edits aren't tool calls NoClick sees, so Before can't match them, and its words reach NoClick only when its turn ends, so the update carries the call without a reason.

Parameters
*toolsstr
Usage
support = noclick.Agent.from_name("support")
refunder = support.spawn(
    "Refund order 1841 if it's within our 30 days, then email the customer.",
    tools=["stripe.create_refund", "gmail.send_email_message"],
    updates=[noclick.Before("stripe.create_refund")],
    conversation="ticket-1841",
)
Attributes
toolslist[str]

The tool patterns it covers.

tools

Every

noclick.Every(interval)

An update the parent requires at least this often while its child works: Every with an interval in seconds (or a timedelta), 60 seconds or more. When that long has passed since the child's last update, NoClick asks it for one (a coding harness by steering its running turn, an sdk agent before its next model call), and the tell_parent it answers with is the every update. A child that's only waiting (for a wake-up, for a person) isn't asked.

Parameters
intervalint | float | timedeltarequired
Usage
planner = noclick.Agent.from_name("planner")
migrator = planner.spawn(
    "Move every customer from the old CRM to the new one, in batches of 200.",
    tools=["hubspot", "salesforce"],
    updates=[noclick.Every(1800)],
    conversation="crm-move",
)
Attributes
interval_sint

How often, at most, the parent hears while the child works.

interval_s

Agents

noclick.Agents()

The agents capability, for an agent's tools: the same as the string "agents", narrowed with Agents.only(...). It gives a coordinator six tools, within its own scope, in its own conversations (in a channel, agents work by mentions instead): spawn (a child of its own, whose updates wake it in the conversation it spawned from), message (any agent it can reach: a user's agent reaches that user's and the account's, an account agent the account's; it waits up to 50 seconds for the reply, and a reply that takes longer, or one it doesn't wait for, is a job whose done update wakes it), ask (as agent.ask), status (an agent's or a job's), list (as Agent.list filters) and cancel (a job it started). Its work runs in the threads of the user it's acting for, and its children get the capability only when it lists it for them. Policy names them agents.spawn ... agents.cancel (agents.*); the model sees agents__spawn, agents__message and so on, and its tools count toward an agent's 64.

Usage
coordinator = noclick.Agent.from_name(
    "coordinator", create_if_missing=True,
    instructions="You run our launches. Spawn a researcher for each competitor, then write the plan.",
    tools=["search", noclick.Agents()],
)
Attributes
toolslist[str]

The capability's tools this gives.

tools

onlyclassmethod

noclick.Agents.only(*tools) -> Agents

Some of the capability's tools: spawn, message, ask, status, list and cancel.

Parameters
*toolsstr
Returns
Agents

The capability narrowed to those tools, for tools= (on the wire, "agents.spawn", ...).

Usage

A supervisor that watches and asks but never starts work:

supervisor = noclick.Agent.from_name(
    "supervisor", create_if_missing=True,
    instructions="You check on our agents every morning and report anything stuck.",
    tools=[noclick.Agents.only("list", "status", "ask")],
)

Platform

noclick.Platform()

NoClick itself, for an agent's tools: the same as the string "noclick", narrowed with Platform.only(...). It gives the agent what a connected app gets on NoClick's MCP server: search and fetch (NoClick's guides and your account's objects), api_describe and api_call (any API operation), and quote, buy and delete. They act as the turn's key and the user it acts for, never more than that key may, judged by the same gate as any connected app: a purchase or a deletion waits for the account's owner, who approves it on NoClick with its exact price or what goes with it (the turn waits on them, and the call's outcome is the conversation's next turn); a secret's value never comes from the conversation; and a turn answering someone outside the agent's scope (an email, a caller) asks a person before acting outside the account. A long action it starts is an operation, which wake_me waits on. Policy names them noclick.search ... noclick.delete; the model sees noclick__search and so on, and they count toward an agent's 64 tools. Watchers see an approval and an operation event for them (agent.stream, agent.watch).

Usage
ops = noclick.Agent.from_name(
    "ops", create_if_missing=True, tools=[noclick.Platform()],
    instructions="You run our NoClick account: numbers, domains and agents.",
)
ops.message("Get us a phone number in the 415 area code.")  # buys once you approve
Attributes
nameslist[str]

What it's narrowed to: capability groups and API areas (empty: all of NoClick).

names

onlyclassmethod

noclick.Platform.only(*names) -> Platform

Part of NoClick: capability groups (read, build, deploy, spend, delete) bound what kind of call it makes, and API areas (a key's scopes, like domains, phone or agents) which part of the API it reaches; each narrows only its own kind. Without spend it isn't offered quote and buy, without delete not delete. Names are checked when the agent is saved.

Parameters
*namesstr
Returns
Platform

NoClick narrowed to those names, for tools= (on the wire, "noclick.read", ...).

Usage

An agent that keeps your domains, and buys new ones once you approve:

registrar = noclick.Agent.from_name(
    "registrar", create_if_missing=True,
    instructions="You look after our domains and their DNS.",
    tools=[noclick.Platform.only("domains")],
)

One that only reads, to answer questions about the account:

analyst = noclick.Agent.from_name(
    "analyst", create_if_missing=True, instructions="You explain our usage and spend.",
    tools=[noclick.Platform.only("read")],
)

Attachment

class Attachment

A file on a message, kept as an account file at url. kind is image, audio, video or file; a voice clip has its duration_ms and its waveform's peaks (loudness from 0 to 1). Audio is kept as transcription reads it (a browser's WebM recording as MP3). A file an agent's conversation shows (in history, or a file event) has its name, url and mime_type, and no id: it was sent. The account's files (noclick.attachments()) have their id, to send again.

Attributes
namestr
urlstr
mime_typestr
idstr | None
size_bytesint | None
kindstr
voicebool
duration_msint | None
peakslist[float] | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

attachments

noclick.attachments(*, query=None, limit=30, client=None) -> list[Attachment]

The account's files, newest first: what its messages carried, the images and videos it made, its calls' recordings. A message to an agent sends any of them again in its files. The account's alone: read as a user, it's refused.

Args: query: Keeps the files whose names contain it (any case). limit: How many, 1 to 100.

Parameters
querystr | Nonedefault None
limitint

Return at most this many.

(default 30)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Attachment]

Each file as an Attachment with its id, name, url, mime_type and kind.

Usage
[report] = noclick.attachments(query="q3")
analyst.message("What changed since this?", files=[report])