Users
A user is someone your product acts for. Everything you do for them is scoped to them.
A user is someone your product acts for: a person or a company. You know them by the id your product already uses for them, and NoClick uses that same id. A user's connected accounts, conversations, inboxes and spend belong to that user and nobody else.
Create a user
Create one when someone signs up to your product, with the id you already use for them:
import noclick
user = noclick.User.create(
"user_42", # your id for them
name="Ana Lima",
email="ana@example.com",
spend_cap_usd_per_month=20,
)You don't have to create users up front. from_id returns a lazy reference, and with create_if_missing=True the user is created the first time it's used. If two requests race to create it, both get the same user:
user = noclick.User.from_id("user_42", create_if_missing=True, email="ana@example.com")The fields you pass are the user's definition, as with an agent: on a user that already exists, the ones that differ are updated to match, so running this at every sign-in keeps the email you show and the one NoClick emails in step. A field you leave out is never changed, and None clears one.
Where the user is
Give a user their timezone, as an IANA name, and every agent acting for them knows what "tomorrow morning" means there:
user = noclick.User.from_id("user_42", create_if_missing=True, timezone="America/Sao_Paulo")Every turn of an agent acting for the user starts with the current date and time in the agent's own timezone if it has one, else the user's, else UTC. A schedule written noclick.Cron("0 8 * * *", tz="user") fires at 8am wherever the agent's user is, and follows them when user.update(timezone=...) moves them: from its next run, it fires in their new zone. timezone converges like the other fields, None clears it, a name that isn't an IANA timezone raises noclick.InvalidRequest, and a user acting for themselves (their session, or a key pinned to them) may change their own.
The user's id
A user's id is yours. NoClick keeps it exactly as you give it and never issues another, so user_42 and auth0|123 work as they are. An id is:
- 1 to 255 characters of printable ASCII, so no spaces;
- free of
/; - anything but
allandnone, which list filters use, and.or.., which URLs resolve away.
Any other id raises noclick.InvalidRequest naming the rule it breaks. An id is unique among your live users, so creating the same one twice raises noclick.Conflict, whose existing_id is that id.
If your own id for someone changes, change theirs with user.update(id=...). Everything the user has follows them.
Act for a user
Inside noclick.as_user, every call acts for that user:
with noclick.as_user(user):
slack = noclick.Connection.from_name("slack", integration="slack", create_if_missing=True) # Ana's Slack, not yours
inbox = noclick.Inbox.create(local_part="ana-support")as_user takes a User or the user's id: noclick.as_user("user_42") does the same.
To act for a user in a single call, most methods take user=, which wins over as_user.
as_user works the same in async code (async with), and it follows your code into tasks and asyncio.to_thread. A thread you start yourself (with threading.Thread or a ThreadPoolExecutor) starts without it, and its calls act for your account. Pass user= in code that runs on your own threads.
Outside as_user, calls act for your account itself. Your own connections, agents and inboxes live there.
An object keeps acting for the user it came from. A Connection you listed inside as_user(user) still belongs to that user when you use it later, outside the block.
Shared agents and a user's own
An agent created outside as_user belongs to your account and is shared. When a user messages it, the conversation is that user's, but the agent's tools act in your account's connections, never the user's.
An agent created inside as_user belongs to that user alone and uses that user's connections: give each user their own agent to act in their own accounts. See Agents.
A company and its people
A user is one boundary: their connections, conversations and data are their own. A company with several people fits two ways, and you choose which in your own data model:
- The company is the user. Everyone at Acme acts as
acmeand shares its connections; reach a particular person withnoclick.Human(email=...). - Each person is a user, and the company one too. Dana's
gmailis hers, and the company'steam-slackjoins her agent's tools by sharing it (invite-only beta). Which people see which company connections is yours to decide: NoClick honours what your account puts in each agent's tools.
Find users
noclick.User.list(q="ana") # prefix of the id, name or email
noclick.User.list(status="suspended")
page = noclick.User.list_page(limit=50) # one page; pass page.next_cursor back as cursor=
user = noclick.User.get("user_42")Update, suspend or delete
user.update(name="Ana Lima-Souza", phone=None) # None clears a field
user.update(id="ana-lima") # a new id; a taken one raises noclick.Conflict
user.suspend()
user.delete()Nothing can act for a suspended user: every call made for them fails with noclick.PermissionDenied, whether it names the user or comes from the user's session or a key pinned to them. Their history stays readable. Work already set up for them keeps going, though: their inboxes still receive mail and wake their agents, their phone numbers still answer, and their channels still run. Delete those too if the user must go quiet. Reactivate a user from their page in the console (over HTTP, PATCH /v1/users/{id} with "status": "active").
Deleting a user marks them deleted and frees their id for reuse, and that's all it does for now: what was set up for them isn't stopped or revoked. From then on no call can act for them (each fails with noclick.NotFound), so nothing they hold can be reached to delete. Delete what acts for them first, inside as_user: their agents (noclick.Agent.list(user_filter=user)), which ends their triggers, and their connections (noclick.Connection.list()), which revokes their grants at each provider, as the assistant example does; then their inboxes, phone numbers and channels if they have any.
What deleting a user stops, and what it keeps
In the beta, deleting a user leaves nothing acting for them:
- their connections' grants are revoked at each provider, so the tokens stop working there too;
- their agents' triggers stop: schedules, webhooks, app events and queues no longer wake them;
- their inboxes stop receiving mail, and their phone numbers are released (billing stops).
What's kept is the record and the data. Their operations, tool calls, conversations, usage and spend stay readable with user_filter, for your books, and the data their databases, buckets, repos, volumes and memory hold stays until you delete it. user.delete(export=True) hands the user a copy first.
Export a user's data
user.export() packs up everything the user holds, from agents and conversations to databases and files, in forms that work without NoClick. user.delete(export=True) exports first and deletes the user once the export is stored (if it fails, nothing is deleted):
bundle = user.delete(export=True).wait()
print(bundle.url)See Export for what the bundle holds.
Cap a user's spend
spend_cap_usd_per_month limits what everything done for a user may cost in a calendar month: agent turns, tool calls, sandboxes and messages. Once the user reaches it, new work for them fails with noclick.SpendCapExceeded. See Spend and limits.
Pause a user's spending
Pausing stops everything that costs money for one user until you resume them, and tells them what they can do about it, in your words. Your billing decides when: pause a user when their balance runs low or a payment fails, and resume them when they pay. NoClick never pauses anyone on its own. Charging your users shows the whole loop.
user = noclick.User.from_id("user_42")
user.pause_spending(
message="You've used this month's credits.",
action=("Top up", "https://acme.com/billing/top-up?user={{user.id}}"),
)
print(user.spending.paused, user.spending.since)
user.resume_spending() # they paid: what was held for them runs nowmessage is what they're told, up to 500 characters, and action a (label, url) pair for your checkout or billing page: a label up to 40 characters and an https URL. Both may use {{user.id}}, {{user.name}}, {{user.email}} and {{user.metadata.<key>}}, filled in when you pause; a field the user doesn't have raises noclick.InvalidRequest. Leave message out and the default you set in the console (Settings, then Paused spending) is used, and leave action out for the default's button; with no message and no default either, the call raises noclick.InvalidRequest. In a link, each filled-in value is percent-encoded, unless one placeholder is the whole link ({{user.metadata.checkout_url}}), which goes in as it is. Keep the link out of message: a phone call reads message aloud, and everywhere else shows action as a button or a link after it.
While a user is paused
Nothing new that costs money starts for them. What happens instead depends on where it would have started:
| Where | What happens |
|---|---|
| Your code, acting for them | The call raises noclick.SpendingPaused (402) before doing any work. Its user_message and action are yours to show them; its message and next_step are for you. |
| A message they send an agent: an email to its inbox, a post in their channel | NoClick answers with your message and action, once per conversation while they're paused (a reply to their email, a note in their channel), without running the agent, and holds the message. Mail someone else sends their agents is held too, without a word. NoClick's answers aren't charged. |
| A call to a number whose agent works for them, or from them to one that knows them as one of your users | The call is answered, hears your message and ends. A call can't be held. |
| Their agents' schedules, webhooks and app events | Skipped, as for a paused agent: a scheduled run isn't made up later, a webhook is answered 409 so its sender can retry, and an app event doesn't wake the agent. |
| Their agents' queue triggers | Messages wait on the queue with their attempts unchanged, and the agent works through them after you resume. |
| A wake-up that comes due | Held, like their messages. |
| Work under way when you pause | Finishes the step it's on (the model call or tool run in progress, the reply a call is speaking) and stops: the next tool call doesn't run, nor the next model call on NoClick's key. A turn ends with spending_paused, and the user is told where they were talking. |
What they already have keeps running and billing: their phone numbers, domains, buckets, volumes and sandboxes. Pausing isn't cancelling: delete what should stop. Everything that costs nothing still works: reading their conversations, runs, files and usage, creating and changing their agents and connections, and writing to their storage.
Held messages run when you resume, in the order they arrived, each as the turn it would have been and starting with when it arrived, so the agent knows it waited. A user's held messages are kept for 24 hours, up to 50 at a time; one that comes past either is still answered with your message, and stays in its inbox or channel, but doesn't run. user.spending.held is how many are waiting.
What stops a user's work, in order
Pausing is one of four things that can stop work for a user, and they're checked in this order:
- Suspended (
user.suspend()): nothing acts for them at all, withnoclick.PermissionDenied. - Spending paused: your billing's call about this user, with
noclick.SpendingPaused, and they're told what to do. - A spend cap reached, the user's, their agent's or the key's, with
noclick.SpendCapExceeded. - Your account's funds used up, with
noclick.PaymentRequired; your users are told only that it isn't available.
Pausing an agent is something else: it stops one agent's triggers, whoever it works for. Your account's own work, and visitors to an account agent's shared page, belong to no user, so pausing a user never touches them; a page sharing one of the user's own agents bills that user, so while they're paused its visitors read that the agent isn't available.
Only a key acting for your whole account can pause or resume a user, and so can you, on the user's page in the console: never the user's own session or a key pinned to them. Pausing a paused user changes the message and action and keeps since and what's held; resuming a user who isn't paused changes nothing. Each pause and resume sends your webhooks user.spending.paused or user.spending.resumed, and noclick.User.list(spending="paused") lists everyone you've paused, to check against your billing.
Verify a user's phone
To prove a user owns a phone number, text them a code and check what they type back:
user.verify_phone(phone="+14155550100")
try:
user.check_phone(code_they_typed)
print(user.phone_verified_at)
except noclick.InvalidRequest as err:
print(err.message) # That code isn't right.An expired code raises noclick.Expired, and too many wrong tries raise noclick.RateLimited.
Ask the user something
user.human reaches the user at their email address. Ask them something, get their approval, or ask them to connect an account. See Asking people.
approved = user.human.approve("Send the weekly summary to your team?").wait()