Calling integrations

Find a service, see what it can do, and run its operations, including the ones that need no account and no connection.

An integration is a service NoClick can operate, like gmail, slack or hubspot. Each has operations, such as send_email_message or send_message_to_channel. An operation that needs an account runs through a connection, so it always acts in exactly that account. The few that need no account, like a web search, need no connection either: they run on NoClick's own key.

Find a service

import noclick

for integration in noclick.Connection.integrations("mail"):
    print(integration.name, integration.label, integration.connect_with, integration.operation_count)

Connection.integrations() lists every integration NoClick can operate. Pass a query to keep the ones whose name or label contains it, in any case. account_free_operation_count says how many of its operations need no account, which is 0 for most.

connect_with lists the ways a person connects an account, one or more of: "oauth" when they sign in to the service, "key" when they paste a key or token, "qr" when they scan a code, and "purchase" for a phone number, which you buy rather than connect. A service that takes either a sign-in or a key lists both (["oauth", "key"]), so test with "oauth" in integration.connect_with. credential_types names the exact credentials an integration accepts, the value credential_type= takes when you bring a key you hold.

See what it can do

slack = noclick.Connection.from_name("slack", integration="slack", user="user_42")

for op in slack.operations(search="message"):
    print(op.name, op.read_only, op.description)

send = slack.operation("send_message_to_channel")
print(send.parameters)   # a JSON schema of its arguments

operations() lists what the connection's integration can do. search keeps the operations whose name or description contains it, in any case. operation() reads one by name, and raises noclick.NotFound for a name the integration doesn't have.

Both read the catalog, not the account. A connection named with integration=, like slack here, isn't loaded or created to answer them, so they work before the user has connected anything, and before the connection even exists. Without integration=, the connection is loaded first to learn which integration it is.

Each operation has:

  • parameters: a JSON schema of its arguments.
  • read_only: a hint from the operation's name (reads start with list_, get_, fetch_, search_ and the like), not a guarantee that nothing changes.
  • needs_account: False when it runs on NoClick's key with no account at all, and so with no connection. An operation that needs an account only for some arguments says True.
  • scopes: the OAuth scopes it needs, where NoClick knows them.

Run an operation

gmail = noclick.Connection.from_name("gmail", user="user_42")

result = gmail.run("send_email_message", {
    "to": ["ana@example.com"],
    "subject": "Your weekly summary",
    "body": "Three invoices were paid this week.",
})

run returns the operation's data, what the service answered, as plain data. The connection decides whose account it runs in: here, the gmail connection of user user_42. When the service refuses the call (a channel that doesn't exist, an account without access), run raises noclick.UpstreamError in the service's own words, with its HTTP status as upstream_status.

noclick.run does the same by ref, the way an agent's tools name it: noclick.run("gmail.send_email_message", {...}, user="user_42") runs through that user's gmail connection.

Operations on NoClick's key

Some operations need no account, because they run on NoClick's own key: a web search, public data. You run them by name. There's no connection to create, and nothing for anyone to connect:

results = noclick.run("exa.search", {"query": "invoice automation for small businesses"})

A ref is "name.operation". When no connection has that name, exa means the integration, and its operations that need no account run on NoClick's key. The same ref works as a tool, for your account's agents and for every user's:

with noclick.as_user("user_42"):
    researcher = noclick.Agent.from_name(
        "researcher",
        create_if_missing=True,
        instructions="You look companies up on the web before a sales call.",
        tools=["exa.search"],
    )

A bare "exa" gives the agent every Exa operation that needs no account. account_free_operation_count says how many an integration has, and needs_account on each operation says which. An operation that needs an account doesn't run this way: noclick.run("exa.create_monitor", {...}) raises noclick.InvalidRequest, telling you to create a connection named exa.

Runs on NoClick's key are billed to your account like any other tool run, and count toward the acting user's spend cap. Your policy judges them by the same names, like exa.search.

Bring your own key

To run them on your own key, create a connection named exa that holds it:

noclick.Connection.create("exa", credential_type="exa_api_key", data={"api_key": "..."})

A connection with the integration's name wins. From then on the same refs use it: noclick.run("exa.search", {...}) and tools=["exa.search"] run on your key, and so do Exa's operations that need an account. Create it inside as_user to give one user a key of their own.

A pending connection

A connection nobody has connected yet still runs its operations that need no account, on NoClick's key. The rest raise noclick.NeedsConnection until it's connected, as the next section shows.

When nothing is connected

If the connection is still pending and the operation needs an account, run raises noclick.NeedsConnection. The error carries the connection's link and the operation that finishes when the user connects:

try:
    gmail.run("send_email_message", args)
except noclick.NeedsConnection as err:
    notify_user(err.connect_url)
    gmail.wait()                                  # returns once they've connected
    gmail.run("send_email_message", args)

If the account was revoked at the provider, run raises noclick.CredentialDisconnected instead, with a link to reconnect as connect_url. Both errors name the connection and its integration.

Look up IDs

Many operations take an ID, like a Slack channel or a GitHub repository. lookup lists the values the connected account allows, with labels you can show in a picker:

slack = noclick.Connection.from_name("slack", user="user_42")
options = slack.lookup("channel", search="gen")

for option in options:
    print(option.value, option.label)

Pass options.next_page_token back as page_token= for more. Some lookups depend on other arguments (a sheet inside a spreadsheet); pass those as arguments.

Policy and approvals

Your policy can forbid an operation (noclick.PolicyDenied) or require a person to approve it first (noclick.ApprovalRequired). Rules name the integration's operation (gmail.send_email_message), whichever connection runs it, or none, on the user's account or on NoClick's key. A connected account can also carry approval rules of its own, which your account's owner sets in the NoClick app. Once the call is approved, repeat it with the same arguments and it runs once. For a policy approval, pass the error's approval_id on the retry; Policy and approvals shows the whole flow.

Letting an LLM run operations

To hand operations to a model as tools, with results the model can read and no exceptions to catch, list them in a toolkit, or in a hosted agent's tools. The refs are the ones noclick.run takes: give the model "exa.search" beside "gmail" and it can search with Exa, with no connection and nothing for the user to connect. The search and fetch capabilities stay the short way to give a model the web. exa gives it Exa's own operations, and moves to your key when you bring one.

Every method is in the reference: noclick.run, Connection.integrations, Connection.operations, Connection.operation, Connection.run and Connection.lookup, with the records they return, IntegrationInfo and IntegrationOperation.