Connection
Connections: named links to your users' accounts on other services, and what those services can do.
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
if not gmail.connected:
send_to_user(gmail.connect_url)
gmail.wait()
gmail.run("send_email_message", {...})A connection is a name in one scope (a user, or your account's own). Code, toolkits and agents name it, so they always act in exactly that account; two accounts on one service are two connections. Reconnecting keeps the name.
An operation that needs no account (exa's web search) needs no connection
either: noclick.run("exa.search", {...}) and tools=["exa.search"] run it
on NoClick's key, for every user. A connection named exa holding your own
key takes over the same refs. Connection.integrations() finds a service,
and operations() says what a connection's service can do.
class ConnectionA named connection to an account on another service. Secrets never come
back; token() mints an OAuth access token when the key has
credentials:token.
status is pending until someone connects an account at
connect_url, connected after, and disconnected once the account
is revoked (connect_link() mints a link to reconnect it). Operations
that need no account run on NoClick's key even while it's pending.
The accounts connected in the NoClick app are connections too, in your
account's own scope: origin is "app" and the id is
credential:<id> (their name is the app's, so refer to them by id).
They run, look up, verify and take require_approval like any
connection, and an account agent takes one as a tool (the Connection,
or "credential:<id>", "credential:<id>.operation"; a user's agent
can't); the app connects, reconnects and removes them. One shared with
your account has access "view" or "edit": it runs, but its
token and approval rules stay its owner's.
An app's preview only reads through a connection: an operation that writes
in its account is refused (noclick.PermissionDenied) unless the
connection is a test account (mark_test_account()).
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | |
| integration | str | |
| credential_type | str | None | |
| user_id | str | None | |
| status | str | |
| origin | str |
|
| access | str | Your account's access: |
| approval_operations | list[str] | Operations its owner approves call by call wherever it runs ( |
| account | str | None | The connected account as the provider named it, when proven. |
| test_account | bool | Whether it's a test account, which an app's preview writes through as well as reads. |
| used_by | list[dict[str, Any]] | What holds it: agents (in their tools, as their |
| connected_at | Any | |
| created_at | Any | |
| updated_at | Any | |
| connected | bool | |
| connect_url | str | None | The open link where someone connects (or reconnects) the account, if one is open. |
| verification | Verification | None | |
| console_url | str | None | This connection's kind in the developer console (None off the hosted API). |
from_nameclassmethod
noclick.Connection.from_name(
name, *, integration=None, create_if_missing=False, credential_type=None,
data=None, redirect_url=None, reason=None, send_to=None, user=..., client=None
) -> ConnectionA lazy reference to the connection called name in the current
(or given) user. With create_if_missing=True it's created on
first use, for integration (a name, or an IntegrationInfo):
holding data when given (a key you hold), else pending with a
connect link (the link options are create's). An existing
connection is used as it is. operations(), operation() and
events() read integration's catalog without loading or
creating it.
NoneFalseNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A connection that loads, or is created, the first time it's used. Nothing is sent yet.
Refer to a connection that already exists:
gmail = noclick.Connection.from_name("gmail", user="user_42")
print(gmail.status) # loads it now; noclick.NotFound if there's noneOr create it the first time it's used, pending with a link for the user. The same line works for a new user and a returning one:
with noclick.as_user("user_42"):
gmail = noclick.Connection.from_name(
"gmail", integration="gmail", create_if_missing=True,
reason="to send your weekly summaries",
)
print(gmail.status, gmail.connect_url) # pending https://noclick.com/credential/provide/...With a key you already hold, it's created holding that key (and never overwrites one):
crm = noclick.Connection.from_name(
"crm", integration="affinity", data={"api_key": "..."},
create_if_missing=True, user="user_42",
)from_idclassmethod
noclick.Connection.from_id(connection_id, *, client=None, user=...) -> ConnectionA lazy reference to a connection by NoClick id, in the current (or
given) user; credential:<id> for an account connected in the app.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
A connection that loads the first time you read a field. Nothing is sent yet.
connection = noclick.Connection.from_id("3f1d2c4b-8a7e-4f6d-9b2a-1c5e7d9f0a31", user="user_42")
print(connection.name, connection.status)
slack = noclick.Connection.from_id("credential:9b2a1c5e-7d9f-4a31-8f6d-3f1d2c4b8a7e")
slack.run("send_message", {"channel": "#ops", "text": "Deployed."})createclassmethodasync
noclick.Connection.create(
name, *, integration=None, credential_type=None, data=None, redirect_url=None,
reason=None, send_to=None, user=..., client=None
) -> ConnectionCreate a connection called name (unique per user; a duplicate
raises noclick.Conflict with existing_id). With data it holds that
credential at once (validated and probed first); without, it's pending
and connect_url is where someone connects it. send_to
("user", an email, or a dict) also delivers the link.
NoneNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new connection: pending with its connect_url, or connected
(with its verification) when you passed data.
Create a pending connection and show the user its link:
with noclick.as_user("user_42"):
slack = noclick.Connection.create("slack", integration="slack", reason="to post your team's standup")
print(slack.connect_url)Or create it holding a key you already have. A key the provider
rejects raises noclick.InvalidRequest and saves nothing:
crm = noclick.Connection.create("crm", integration="affinity", data={"api_key": "..."}, user="user_42")
print(crm.status) # connectedA model provider's connection is what an agent on a CLI harness thinks with (its
model_connection). Pass a key as api_key; a Claude or ChatGPT plan is
signed in only at the link, and only on a user's own connection:
claude = noclick.Connection.create("claude", integration="anthropic", data={"api_key": "sk-ant-..."})
with noclick.as_user("user_42"):
plan = noclick.Connection.create("claude", integration="anthropic")
print(plan.connect_url) # "Paste an Anthropic key" or "Sign in with Claude"If the name may be taken, the conflict names the connection already there:
try:
slack = noclick.Connection.create("slack", integration="slack", user="user_42")
except noclick.Conflict as exc:
slack = noclick.Connection.from_id(exc.existing_id, user="user_42")getclassmethodasync
noclick.Connection.get(ref, *, user=..., client=None) -> ConnectionLoad the connection with this id or name now; an unknown one raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The connection, loaded.
with noclick.as_user("user_42"):
gmail = noclick.Connection.get("gmail")
print(gmail.status, gmail.account)listclassmethodasync
noclick.Connection.list(
*, integration=None, origin=None, limit=None, user_filter=None, user=...,
client=None
) -> list[Connection]The current user's connections (the account's own outside
as_user), or those user_filter covers: named ones newest first,
then, in the account's own scope, the accounts connected in the app.
origin keeps only "api" or "app" ones.
NoneNoneReturn at most this many.
None)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.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching connection (or the first limit), fetched page by page.
with noclick.as_user("user_42"):
for connection in noclick.Connection.list():
print(connection.name, connection.integration, connection.status)
every_gmail = noclick.Connection.list(integration="gmail", user_filter=noclick.ALL_USERS)
from_the_app = noclick.Connection.list(origin="app")list_pageclassmethodasync
noclick.Connection.list_page(
*, cursor=None, limit=None, integration=None, origin=None, user_filter=None,
user=..., client=None
) -> PageOne page of connections; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)NoneNoneWhich users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the connections and whose next_cursor is None on the last page.
page = noclick.Connection.list_page(limit=50, user_filter=noclick.ALL_USERS)
while True:
for connection in page.data:
print(connection.user_id, connection.name, connection.status)
if page.next_cursor is None:
break
page = noclick.Connection.list_page(limit=50, user_filter=noclick.ALL_USERS, cursor=page.next_cursor)integrationsclassmethodasync
noclick.Connection.integrations(
query=None, *, kind=None, client=None
) -> list[IntegrationInfo]The services NoClick can operate and the model providers its agents think
with: every one, or those whose name or label contains query (ignoring
case). kind="app" keeps the services, whose operations are tools;
kind="model" keeps the model providers (anthropic, openai,
openrouter, ...), which an agent names as its model_connection. The
catalog is your account's, the same for every user.
NoneNoneThe client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Each one's name (what integration= takes), label, kind, operation_count,
account_free_operation_count (how many run with no account, by the service's name),
connect_with (how a person connects an account) and subscription (whether its
link also offers signing in with a Claude or ChatGPT plan).
Find a service, then make a connection to it:
[crm] = noclick.Connection.integrations("affinity")
print(crm.label, crm.connect_with) # Affinity ['key']
with noclick.as_user("user_42"):
affinity = noclick.Connection.from_name("crm", integration=crm, create_if_missing=True)
print(affinity.connect_url)Or list every service a person connects by signing in:
for service in noclick.Connection.integrations():
if "oauth" in service.connect_with:
print(service.name, service.label)Or the services with operations that need no account, which run by the service's
name with no connection (noclick.run("exa.search", ...)):
for service in noclick.Connection.integrations():
if service.account_free_operation_count:
print(service.name, service.account_free_operation_count) # exa 4Or the model providers an agent's model_connection can name:
for provider in noclick.Connection.integrations(kind="model"):
print(provider.name, provider.connect_with, provider.subscription) # anthropic ['key'] Trueonly
connection.only(*operations) -> ConnectionOperationsA tool ref to some of its operations, for tools= (gmail.only("send_email_message"));
a lazy create_if_missing connection is still created first.
A ref to just those operations of this connection, to list in tools=; bind(...) on it fixes
arguments the model mustn't choose.
A large integration doesn't fit an agent whole, so name the operations it needs:
with noclick.as_user("user_42"):
stripe = noclick.Connection.from_name("stripe", integration="stripe", create_if_missing=True)
support = noclick.Agent.from_name(
"support", create_if_missing=True,
instructions="You answer the user's billing questions.",
tools=[stripe.only("list_invoices", "search_charges"), "search"],
)checkout
connection.checkout(repository, *, branch=None, path=None, push=None) -> RepoCheckoutOne of this connection's repositories (owner/name on its git host; a
github connection) checked out in an agent's workspace, for Agent(repos=[...]):
cloned into /workspace/ (the repository's name when omitted) at a
conversation's first turn, on branch (its default when omitted). The agent's git
reaches this repository only, through NoClick, with a key made for its sandbox and
revoked when the sandbox ends; the connection's own token never enters it. push
is the branches it may push to, as globs: NoClick refuses a push to any other ref,
and without push the checkout is read-only. gh reads the repository
(gh api repos/<owner>/<name>/...) and nothing more, since its writes could move a
branch past push: give the agent the connection's operations
(github.only("create_pull_request")) for pull requests and issues, which your policy
judges. Sends nothing; a lazy create_if_missing connection is created when the agent
is saved, and the connection must be connected before a turn can clone.
NoneNoneNoneA RepoCheckout to list in an agent's repos.
A docs agent that pushes only to branches it names noclick/...:
github = noclick.Connection.from_name("github", integration="github", create_if_missing=True)
writer = noclick.Agent.from_name(
"docs-writer",
create_if_missing=True,
instructions="You fix the docs issues you're given in /workspace/docs and open a pull request.",
harness="claude_code",
model_connection="claude",
tools=[github.only("create_pull_request")],
repos=[github.checkout("acme/docs", push="noclick/*")],
)Read-only, on a branch, in a folder of your choosing:
github = noclick.Connection.from_name("github")
reference = github.checkout("acme/api", branch="release-2.4", path="api")operationsasync
connection.operations(search=None) -> list[IntegrationOperation]The operations of this connection's integration: every one, or those
whose name or description contains search (ignoring case).
NoneEach operation's name, description, the JSON schema of its parameters,
needs_account (False when it runs on NoClick's key, with no connection or on a
pending one) and read_only, a hint from its name rather than a guarantee.
See what a user's Slack can do before anyone has connected it. With integration=
nothing is loaded or created to answer:
with noclick.as_user("user_42"):
slack = noclick.Connection.from_name("slack", integration="slack", create_if_missing=True)
for op in slack.operations(search="message"):
print(op.name, op.read_only, op.description)Find the operations that need no account. They run by the integration's name with no
connection at all (noclick.run("exa.search", ...)), and reading them creates nothing:
exa = noclick.Connection.from_name("exa", integration="exa")
keyless = [op.name for op in exa.operations() if not op.needs_account]operationasync
connection.operation(name) -> IntegrationOperationOne operation of this connection's integration; an unknown name raises noclick.NotFound.
The operation, with the JSON schema of the parameters that run takes for it.
Read an operation's parameters before running it:
slack = noclick.Connection.from_name("slack", integration="slack", user="user_42")
send = slack.operation("send_message_to_channel")
print(send.description, send.parameters["required"])
slack.run("send_message_to_channel", {"channel": "#general", "text": "Standup in 5"})eventsasync
connection.events() -> list[IntegrationEvent]The app events this connection's integration can wake an agent with, for noclick.OnEvent.
A poll (new mail in Gmail, say) is no event here: only what the app pushes as it happens.
Every event, with its description and the filters it takes
(channel for a Slack message, repository for a GitHub issue).
slack = noclick.Connection.from_name("slack", integration="slack", user="user_42")
for event in slack.events():
print(event.name, event.description, list(event.filters))connect_linkasync
connection.connect_link(*, redirect_url=None, reason=None, send_to=None) -> ConnectLinkMint a new link where someone connects (or reconnects) this
connection's account. send_to ("user", "owner" for your account's
owner, an email, or a dict) also delivers it.
NoneNoneNoneThe link: its url, when it expires_at, and where it went when
you set send_to. It's also the operation that finishes when
someone connects: link.wait() returns the connected Connection.
The connection's own connect_url is now this link's.
Make a link and show it in your product yourself:
gmail = noclick.Connection.from_name("gmail", user="user_42")
link = gmail.connect_link(redirect_url="https://app.example.com/settings/connections")
print(link.url, link.expires_at)Or have NoClick email it. From code that isn't acting for a user,
send_to can be any email address:
ops_slack = noclick.Connection.from_name("ops-slack")
link = ops_slack.connect_link(send_to="ops@example.com", reason="to post alerts in #ops")
print(link.delivered_via, link.undelivered) # ['email'] []
ops_slack = link.wait()requestasync
connection.request(*, reason=None, send_to='user', redirect_url=None) -> ConnectLinkAsk the user (or send_to) to connect it: a new connect link, emailed. A
connection of your account's own has no user: send it to "owner" (your
account's owner) or an email.
None'user'NoneThe link, already sent: delivered_via says where it went, and
.wait() on it returns the connected Connection.
gmail = noclick.Connection.from_name("gmail", user="user_42")
if gmail.refresh().status == "disconnected":
link = gmail.request(reason="Gmail stopped working. Reconnect it to keep your summaries coming.")
print(link.delivered_via) # ['email']waitasync
connection.wait(timeout=None) -> ConnectionBlock until an account is connected (returns at once when it is). It
follows the connection, not one link: an account connected through any
of its links ends the wait, and a link that lapses after a fresh one was
made (connect_link() or request(), from anywhere) hands the wait
to the fresh one. Raises noclick.OperationExpired when the open link
lapses with no fresh one, or noclick.OperationTimeout after
timeout seconds.
NoneThis connection, connected. After a wait it's reloaded, so account
names the account when the provider says who it is.
Wait in a background job, not while a web request is open:
gmail = noclick.Connection.from_name("gmail", user="user_42")
gmail.wait(timeout=3600)
print(gmail.account) # ana@example.comIf the link lapses first, send a fresh one and keep waiting:
gmail = noclick.Connection.from_name("gmail", user="user_42")
try:
gmail.wait()
except noclick.OperationExpired:
gmail.request(reason="to send your weekly summaries")
gmail.wait()import_async
connection.import_(data, *, credential_type=None) -> ConnectionReplace the connection's credential with one you hold (a rotated
key), validated and probed first. credential_type, when the
integration takes more than one, is one of its credential_types
(Connection.integrations()).
NoneThis connection, holding the new key, with the probe's verification.
The key it replaced is deleted.
Rotate a key:
crm = noclick.Connection.from_name("crm", user="user_42")
crm.import_({"api_key": "..."})
print(crm.status, crm.verification)Or bring your own key for a service whose operations need no account: a connection
named after it runs its refs ("exa.search") on your key instead of NoClick's:
exa = noclick.Connection.from_name("exa", integration="exa", create_if_missing=True)
exa.import_({"api_key": "..."})
print(exa.verification)runasync
connection.run(operation, arguments=None, *, approval_id=None) -> AnyRun one of the integration's operations in this account. An operation
that needs no account (needs_account False in operations()) runs
on NoClick's key while the connection is pending, and needs no connection
at all through noclick.run; any other raises
noclick.NeedsConnection until it's connected and noclick.CredentialDisconnected
once revoked (both carry connect_url). A connection holding your own
key runs every operation on it. Policy may raise
noclick.PolicyDenied or noclick.ApprovalRequired (retry with its
approval_id= once .operation.wait() is True). When the service
refuses the call, it raises noclick.UpstreamError in the service's own
words, with its HTTP status in upstream_status.
NoneNoneThe operation's data, as plain data (what the service answered).
Run an operation in the user's account:
gmail = noclick.Connection.from_name("gmail", user="user_42")
gmail.run("send_email_message", {
"to": ["ana@example.com"],
"subject": "Your weekly summary",
"body": "Three invoices were paid this week.",
})A pending connection runs the operations that need no account at once, on NoClick's key:
exa = noclick.Connection.from_name("exa", integration="exa", create_if_missing=True)
results = exa.run("search", {"query": "invoice automation for small businesses"})If nothing is connected yet, send the user the link and retry once they've connected:
gmail = noclick.Connection.from_name("gmail", user="user_42")
args = {"to": ["ana@example.com"], "subject": "Your weekly summary", "body": "..."}
try:
gmail.run("send_email_message", args)
except noclick.NeedsConnection as err:
notify_user(err.connect_url)
gmail.wait()
gmail.run("send_email_message", args)When your policy holds the call for a person's approval, wait for it and repeat the identical call with the approval's id:
gmail = noclick.Connection.from_name("gmail", user="user_42")
args = {"to": ["ana@example.com"], "subject": "Your weekly summary", "body": "..."}
try:
gmail.run("send_email_message", args)
except noclick.ApprovalRequired as pending:
if pending.operation.wait():
gmail.run("send_email_message", args, approval_id=pending.approval_id)lookupasync
connection.lookup(
field, arguments=None, *, search=None, page_token=None
) -> LookupOptionsList the allowed values of an ID-like field (channels, repos, sheets) in this account.
NoneNoneNoneOne page of options, each with a value to pass and a label to show;
next_page_token is set when there are more.
Fill a picker from the user's own Slack:
slack = noclick.Connection.from_name("slack", user="user_42")
for option in slack.lookup("channel", search="gen"):
print(option.value, option.label)A lookup that depends on another argument (a sheet inside a spreadsheet)
takes it in arguments; pass next_page_token back as page_token for more:
sheets = noclick.Connection.from_name("sheets", user="user_42")
spreadsheet = {"spreadsheet_id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"}
tabs = sheets.lookup("sheet_name", spreadsheet)
if tabs.next_page_token:
more = sheets.lookup("sheet_name", spreadsheet, page_token=tabs.next_page_token)verifyasync
connection.verify() -> Verification | NoneRe-check the account against the provider.
The provider's verdict, also kept as verification: reachable is
True when it accepted the account, False when it refused it, and
None when NoClick can't tell; message says what was found.
gmail = noclick.Connection.from_name("gmail", user="user_42")
check = gmail.verify()
print(check.reachable, check.message) # True Signed in as ana@example.com.require_approvalasync
connection.require_approval(*operations) -> ConnectionFrom now on its owner approves each call of these operations, wherever
it runs: your code, agents, and the app's workflows. "*" means every
operation. It only ever adds to approval_operations; lifting one is
done in the app. On one shared with your account, noclick.PermissionDenied.
The connection, with its approval_operations.
gmail = noclick.Connection.from_name("gmail")
gmail.require_approval("send_email_message", "delete_message")
print(gmail.approval_operations) # ['delete_message', 'send_email_message']mark_test_accountasync
connection.mark_test_account(test_account=True) -> ConnectionMark it as a test account (or, with False, not one). An app's preview only reads
through a connection; through a test account it writes as well, as production does. Only
the account that connected it marks it (noclick.PermissionDenied on one shared with
yours), and it must hold an account (noclick.InvalidRequest while pending).
TrueThe connection, with test_account set.
slack = noclick.Connection.from_name("slack-sandbox")
slack.mark_test_account()
print(slack.test_account) # Truetokenasync
connection.token() -> TokenMint a fresh OAuth access token for calling the provider yourself. A model
provider's connection never hands out its key or plan token
(noclick.PermissionDenied).
The access token, its token_type and when it expires_at; never a
refresh token or an API key.
gmail = noclick.Connection.from_name("gmail", user="user_42")
token = gmail.token()
headers = {"Authorization": f"{token.token_type} {token.access_token}"}deleteasync
connection.delete() -> NoneDelete the connection, revoking its account at the provider where possible.
noclick.Connection.from_name("gmail", user="user_42").delete()run
noclick.run(ref, arguments=None, *, approval_id=None, user=..., client=None) -> AnyRun one operation by its ref, "name.operation", as tools= names it. name
is a connection in the scope the call acts for (the user's, else the account's), else
an integration whose operation needs no account, which then runs on NoClick's key with
no connection at all. A connection named like the integration (exa, holding your
own key) wins, so the same ref then runs on your key. An account connected in the app
is "credential:<id>.operation".
It raises like connection.run: noclick.NeedsConnection (or
noclick.CredentialDisconnected) with the connect_url when the connection has no
working account, noclick.InvalidRequest when the operation needs an account and no
connection has that name, noclick.NotFound when name is neither a connection nor
an integration, noclick.PolicyDenied or noclick.ApprovalRequired (retry with
its approval_id= once .operation.wait() is True), and noclick.UpstreamError
when the service refuses the call (its own words, its HTTP status in upstream_status).
NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The operation's data, as plain data (what the service answered).
Search the web with nothing to create or connect, on NoClick's key:
results = noclick.run("exa.search", {"query": "invoice automation for small businesses"})A name that is a connection runs through it, here the user's own Gmail:
with noclick.as_user("user_42"):
noclick.run("gmail.send_email_message", {
"to": ["ana@example.com"],
"subject": "Your weekly summary",
"body": "Three invoices were paid this week.",
})Bring your own key: a connection named exa holding it takes over the same ref:
noclick.Connection.create("exa", credential_type="exa_api_key", data={"api_key": "..."})
results = noclick.run("exa.search", {"query": "invoice automation for small businesses"})When your policy holds the call for a person's approval, wait for it and repeat the identical call with the approval's id:
args = {"to": ["ana@example.com"], "subject": "Your weekly summary", "body": "..."}
try:
noclick.run("gmail.send_email_message", args, user="user_42")
except noclick.ApprovalRequired as pending:
if pending.operation.wait():
noclick.run("gmail.send_email_message", args, user="user_42", approval_id=pending.approval_id)ConnectionOperations
class ConnectionOperationsSome of a connection's operations as tools (connection.only(...)), with
the arguments bind(...) fixed.
| connection | Connection | |
| operations | list[str] | |
| bindings | dict[str, Any] | The arguments NoClick fills on every call, out of the model's reach. |
bind
connection_operations.bind(**arguments) -> ConnectionOperationsFix arguments of these operations: a bound parameter leaves the tool's schema,
so the model never sees or sets it, and NoClick fills it on every call. A value may
name the user the call acts for: {{user.id}}, {{user.email}},
{{user.name}} or {{user.metadata.<key>}}. A bound value always wins,
over what a model passes through in an operation's free-form parameters too; a
tool whose binding can't be filled (a user with no email, say) isn't offered that
turn. Policy judges the call's final arguments. Every operation listed must take
each bound parameter. Works wherever tools do: agents, toolkits and hosted MCP
servers. Sends nothing.
A new ref with these arguments bound on top of any bound before.
An assistant that can email only its own user, whatever a message it reads says:
with noclick.as_user("user_42"):
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
assistant = noclick.Agent.from_name(
"assistant",
create_if_missing=True,
instructions="You summarize the user's inbox and email them the summary.",
tools=[gmail.only("fetch_emails_from_inbox"), gmail.only("send_email_message").bind(to="{{user.email}}")],
)Pin a shared account connection to each user's own records:
hubspot = noclick.Connection.from_name("hubspot")
crm = hubspot.only("search_contacts").bind(owner_id="{{user.metadata.hubspot_owner}}")
toolkit = noclick.Toolkit([crm, "search"])ConnectLink
class ConnectLink(Operation)A connect page URL that is also the operation tracking it.
- Inherits from Operation.
| url | str | |
| expires_at | Any | |
| connection | str | None | The connection the link fills. |
| delivered_via | list[str] | Channels the link went out on when |
| undelivered | list[dict[str, Any]] | Channels that failed, each with a |
listclassmethodasync
noclick.ConnectLink.list(*, limit=None, **filters) -> list[Operation]Connect-link operations (kind credential.connect), newest first; takes
Operation.list's filters except kind. Rows are operations: the link's
URL is only in the response that created it (and on its pending connection).
Return at most this many.
None)Every matching operation (or the first limit), fetched page by page;
wait() on one returns the connected Connection.
for op in noclick.ConnectLink.list(status="awaiting_human", user_filter=noclick.ALL_USERS):
print(op.user_id, op.summary_text, op.created_at) # ... Connect gmail ...list_pageclassmethodasync
noclick.ConnectLink.list_page(*, cursor=None, limit=None, **filters) -> PageOne page of connect-link operations; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)A page whose data holds the operations and whose next_cursor is None on the last page.
page = noclick.ConnectLink.list_page(status="expired", limit=50)
while True:
for op in page.data:
print(op.summary_text, op.updated_at)
if page.next_cursor is None:
break
page = noclick.ConnectLink.list_page(status="expired", limit=50, cursor=page.next_cursor)summaryclassmethodasync
noclick.ConnectLink.summary(**filters) -> OperationSummaryCount connect-link operations; takes Operation.summary's filters except kind.
The total that match and a count for every status in by_status.
How many of last week's links were used, and how many lapsed:
from datetime import datetime, timedelta, timezone
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
counts = noclick.ConnectLink.summary(created_after=week_ago, user_filter=noclick.ALL_USERS)
print(counts.total, counts.by_status["succeeded"], counts.by_status["expired"])Verification
class VerificationWhether the provider accepted the credential (reachable None = cannot judge).
| reachable | bool | None | |
| samples | list[Any] | |
| message | str | None | |
| hint | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Token
class TokenA short-lived OAuth access token (never a refresh token or API key).
| access_token | str | |
| token_type | str | None | |
| expires_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
LookupOption
class LookupOption| value | Any | |
| label | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
LookupOptions
class LookupOptions(list[LookupOption])A page of options; pass next_page_token back as page_token.
| next_page_token | str | None |
IntegrationInfo
class IntegrationInfoA service NoClick can operate, such as gmail, or a model provider an
agent thinks with, such as anthropic: one of Connection.integrations().
Its name is what integration= takes.
| name | str | |
| label | str | None | |
| kind | str |
|
| subscription | bool | Whether its connect link also offers signing in with the provider's plan (a Claude or ChatGPT subscription) instead of pasting a key. Only a user's own connection takes a plan. |
| connect_with | list[str] | How a person connects an account: |
| operation_count | int | None | |
| account_free_operation_count | int | How many of its operations need no account (0 for most): |
| credential_types | list[str] | The exact credential types it takes, for |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
IntegrationOperation
class IntegrationOperationOne operation of a connection's integration, with the JSON schema of the
parameters connection.run takes for it.
| name | str | |
| description | str | None | |
| parameters | dict[str, Any] | |
| read_only | bool | None | A hint from the operation's name (reads start with |
| scopes | list[str] | The OAuth scopes it needs, where NoClick knows them. |
| needs_account | bool | False when it runs on NoClick's key with no account at all (web search, public data), so it runs with no connection ( |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
IntegrationEvent
class IntegrationEventAn app event a connection's integration can deliver to an agent's
noclick.OnEvent trigger or a channel's subscription: its name
(channel_message, contact_created), what it means, and filters, the
keyword arguments that narrow it, each with what it matches (one marked
Required. must be given).
| name | str | |
| description | str | None | |
| filters | dict[str, str] | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |