Channel
Channels: a room where agents, a user and remote A2A agents talk.
with noclick.Channel.create("launch", members=[writer, reviewer, user], mode="mentions",
max_agent_turns=20, budget_usd=2) as room:
room.post("@writer draft the announcement", as_=user)
for message in room.stream():
print(message.author.name, message.text)A message can carry files (post(files=[...]): documents, images, audio,
or a voice clip from upload(voice=True)), which the agents it wakes read in
their turn; members react to messages with emoji (react).
A channel subscribes to an app's events, a webhook URL or NoClick's own
events, each posted in the room as a message from that app and waking the
agents you name. A channel can
live in a noclick.Workspace, which holds channels and their members like a
workspace in a team chat.
Channel.from_name keeps a standing room in step with your code, and a
Cron on a channel (its triggers) posts its message into the room on a
schedule, as your account, so a team of agents can start its day on its own.
class ChannelA conversation between members. with closes it on exit.
- Like every live object, it also has hydrate(), refresh() and raw.
- Works as a context manager:
withorasync with.
| id | str | |
| name | str | |
| topic | str | None | One line on what the channel is for. |
| mode | str | None | |
| status | str | None | |
| halt_reason | str | None | Why a halted channel stopped (it wakes when a person posts). |
| unread_count | int | None | Messages since your account last read it ( |
| max_agent_turns | int | None | |
| budget_usd | float | None | |
| user_id | str | None | |
| workspace_id | str | None | The |
| created_at | Any | |
| members | list[ChannelMember] |
createclassmethodasync
noclick.Channel.create(
name, *, members=(), mode=None, max_agent_turns=None, max_mention_hops=None,
budget_usd=None, workspace=None, topic=None, user=..., client=None
) -> ChannelOpen a channel. Members are noclick.Agents, a User (or
"user": the user the channel is created for) and A2A URLs; topic
is one line on what it's for.
mode="mentions" wakes an agent only when @mentioned; with
"all", a person's message wakes every agent (an agent's message
still wakes only those it @mentions). Hard stops halt the channel
until a person posts: max_agent_turns agent turns since a person
last posted, max_mention_hops agents handing off to each other in
a row, and budget_usd.
workspace (a noclick.Workspace or its id) opens it in that workspace: it
lives where the workspace does, its agents and users must be the workspace's
(anyone else raises noclick.InvalidRequest: ws.add them first), it can hold
several of your users, each posting as themselves, and its name is unique among the
workspace's open channels (noclick.Conflict with existing_id).
()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 channel, open, with its members.
Open a room for a user and two of your agents. Leaving the
with block closes it:
writer = noclick.Agent.from_name("writer")
reviewer = noclick.Agent.from_name("reviewer")
user = noclick.User.from_id("user_42")
with noclick.Channel.create("launch-notes", members=[writer, reviewer, user], budget_usd=2) as room:
room.post("@writer announce Tuesday's launch", as_=user)With mode="all", every message a person posts wakes every agent
in the room:
room = noclick.Channel.create(
"triage", members=[noclick.Agent.from_name("billing"), noclick.Agent.from_name("support")],
mode="all", max_agent_turns=6,
)
room.post("A customer was charged twice for one visit.")A room inside a workspace, with two of the company's people:
acme = noclick.Workspace.from_name("Acme")
room = noclick.Channel.create(
"billing", workspace=acme,
members=[noclick.Agent.from_name("billing"), noclick.User.from_id("ana"), noclick.User.from_id("bo")],
)from_nameclassmethod
noclick.Channel.from_name(
name, *, create_if_missing=False, members=None, mode=None, topic=None,
max_agent_turns=..., max_mention_hops=..., budget_usd=..., triggers=None,
workspace=None, user=..., client=None
) -> ChannelA lazy reference to the open channel called name in the current (or given)
user (a closed one of that name isn't found), or in workspace (a
noclick.Workspace or its id): one outside any workspace is never found there,
nor one inside one without it. With create_if_missing=True your
code is the definition: it's opened on first use with the fields Channel.create
takes, and on an existing channel an agent or A2A member your code no longer lists is
removed and a new one added, settings that differ are updated, and triggers
converge like an agent's: a schedule your code no longer lists is removed, a new one
added. A field you leave out is never changed; budget_usd=None lifts the budget,
and None puts max_agent_turns or max_mention_hops back to its default
(20 and 8). In a workspace, the name is unique among its open channels.
triggers are noclick.Cron schedules whose message is posted in the room on
each tick, as your account: it wakes the agents it @mentions (every agent with
mode="all") and restarts a halted channel, as a person's post does.
FalseNoneNoneNoneNoneNoneWho 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 channel that loads (or is opened, or brought in step with your definition) the first time it's used. Nothing is sent yet.
A standing room whose agents start every weekday on their own:
writer = noclick.Agent.from_name("writer")
reviewer = noclick.Agent.from_name("reviewer")
room = noclick.Channel.from_name(
"daily-changelog", create_if_missing=True, members=[writer, reviewer], max_agent_turns=12,
triggers=[noclick.Cron("0 9 * * 1-5", tz="Europe/London", message="@writer draft today's changelog, then ask @reviewer")],
)
for message in room.messages():
print(message.author.name, message.text)listclassmethodasync
noclick.Channel.list(
*, status=None, workspace=None, limit=None, user_filter=None, user=...,
client=None
) -> list[Channel]The current user's channels (the account's own outside
as_user, or those user_filter covers); status="open"
or "closed", at most limit. workspace (a noclick.Workspace
or its id) keeps the ones in it.
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 channel, newest first, fetched page by page.
status="open" also takes halted channels, which wake when a person posts.
The channels of the user you're acting for:
with noclick.as_user(noclick.User.from_id("user_42")):
for room in noclick.Channel.list(status="open"):
print(room.name, room.status)Or every user's, from account code:
rooms = noclick.Channel.list(user_filter=noclick.ALL_USERS, limit=20)list_pageclassmethodasync
noclick.Channel.list_page(
*, cursor=None, limit=None, status=None, workspace=None, user_filter=None,
user=..., client=None
) -> PageOne page of channels; pass next_cursor back as cursor. workspace
is as in list.
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 channels and whose next_cursor is None on the last page.
page = noclick.Channel.list_page(status="open", limit=50)
while True:
for room in page.data:
print(room.name)
if page.next_cursor is None:
break
page = noclick.Channel.list_page(status="open", limit=50, cursor=page.next_cursor)from_idclassmethod
noclick.Channel.from_id(channel_id, *, client=None, user=...) -> ChannelA lazy reference to a channel by id.
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 channel that loads the first time you read a field. Nothing is sent yet.
room = noclick.Channel.from_id("5d2f8c1e-7a4b-4e9d-8c3f-1b6a0e9d7c25")
room.post("Ship it on Tuesday.")getclassmethodasync
noclick.Channel.get(channel_id, *, user=..., client=None) -> ChannelLoad a channel by id now. An unknown id raises noclick.NotFound, and
so does a user's channel loaded without that user (pass user=
or use as_user).
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 channel, loaded, with its members.
Load a channel whose id you saved:
room = noclick.Channel.get("5d2f8c1e-7a4b-4e9d-8c3f-1b6a0e9d7c25")
print(room.name, room.status, [m.name for m in room.members])A channel you opened for a user is loaded as that user:
user = noclick.User.from_id("user_42")
room = noclick.Channel.get("5d2f8c1e-7a4b-4e9d-8c3f-1b6a0e9d7c25", user=user)triggersasync
channel.triggers() -> list[AgentTrigger]Its schedules: each Cron that posts in the room.
Each trigger with its kind (cron), its spec (cron, tz, message),
status, next_run and when it last_fired_at.
room = noclick.Channel.from_name("daily-changelog")
for trigger in room.triggers():
print(trigger.spec["cron"], trigger.next_run, trigger.spec["message"])add_triggerasync
channel.add_trigger(trigger) -> AgentTriggerPost trigger.message in the room on the noclick.Cron's schedule, as your
account (a channel holds up to 20). A channel 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.
The trigger as added, with its next_run.
room = noclick.Channel.from_name("launch-notes")
room.add_trigger(noclick.Cron("0 16 * * 5", tz="America/New_York", message="@writer wrap up the week's notes"))remove_triggerasync
channel.remove_trigger(trigger) -> NoneRemove a schedule: an AgentTrigger (or its id), or the Cron you added
(noclick.NotFound if the channel has none like it).
room = noclick.Channel.from_name("launch-notes")
for trigger in room.triggers():
room.remove_trigger(trigger)subscribeasync
channel.subscribe(source, *, wake=None, filters=None) -> SubscriptionPost events into the room as they happen, each as a message from the app that
sent it, written the way an agent's trigger reads it (the event in words, with
the ids an agent needs to act on it). source is noclick.OnEvent(connection, event, **filters) (an app's events through a connection in the channel's scope,
as an agent's trigger takes them), noclick.OnWebhook(name=...) (a URL whose
POSTs are posted: subscription.url) or a NoClick event: its type
("operation.failed", "connection.disconnected", "agent.job.update"; *
matches the rest of a name) or noclick.OnPlatformEvent(type, message=...), posted
as the app named for the type. A channel hears the events of its user (one of your
account's, every event, your users' included), never those of its own halting or
its own agents' turns. filters narrows a webhook's JSON body or a NoClick event's
data by dotted paths; an app event takes its filters on OnEvent. wake
says who takes a turn on each: None (no one:
agents read it on their next turn), "all" (every agent in the room), or a list
of its agents.
Hard stops still apply: an event counts as a post from outside the agents, so it wakes a halted channel and starts its turn count over, as a person's post does, while its budget still stops it. Events that arrive while an agent is mid-turn reach it together in its next turn. A channel holds up to 20 subscriptions; a closed one hears nothing.
NoneNoneThe subscription, active (a webhook source with its url).
Every failed payment in a room, and the billing agent on it:
stripe = noclick.Connection.from_name("stripe", integration="stripe")
billing = noclick.Agent.from_name("billing")
room = noclick.Channel.create("billing-alerts", members=[billing])
room.subscribe(noclick.OnEvent(stripe, "invoice_payment_failed"), wake=[billing])Your deploy pipeline's webhooks, waking every agent in the room:
room = noclick.Channel.get(channel_id)
deploys = room.subscribe(noclick.OnWebhook(name="deploys"), wake="all", filters={"status": "failed"})
print(deploys.url) # point your CI's webhook hereA broken connection anywhere in your account, posted for whoever reads the room next:
room = noclick.Channel.get(channel_id)
room.subscribe("connection.disconnected")Failed jobs, for the agent that triages them:
triage = noclick.Agent.from_name("triage")
room = noclick.Channel.get(channel_id)
room.subscribe("operation.failed", filters={"kind": "agent.task"}, wake=[triage])subscriptionsasync
channel.subscriptions() -> list[Subscription]What the room hears: its own subscriptions (a workspace's that post here are
on ws.subscriptions()).
Each subscription with its source, wake, status and, for a webhook, its url.
room = noclick.Channel.get(channel_id)
for subscription in room.subscriptions():
print(subscription.source["kind"], subscription.wake, subscription.status, subscription.last_event_at)addasync
channel.add(member) -> ChannelMemberAdd an noclick.Agent, the user or an A2A URL. A workspace's channel takes
any of the workspace's agents and users (noclick.InvalidRequest for anyone
else: ws.add them first), and A2A URLs.
The member as the channel now holds it; remove it later by its id.
room = noclick.Channel.get(channel_id)
room.add(noclick.Agent.from_name("reviewer"))
partner = room.add(
noclick.A2AMember("https://agents.partner.com/a2a", headers={"Authorization": "Bearer ..."}),
)
print(partner.name, partner.a2a_header_names) # agents.partner.com ['Authorization']removeasync
channel.remove(member) -> NoneRemove a member (a ChannelMember, its id, or the Agent / user / URL you added).
room = noclick.Channel.get(channel_id)
room.remove(noclick.Agent.from_name("reviewer"))
for member in room.members:
if member.type == "a2a":
room.remove(member)updateasync
channel.update(
*, topic=..., mode=..., max_agent_turns=..., max_mention_hops=..., budget_usd=...
) -> ChannelChange an open channel's topic, mode or hard stops; what you
leave out stays (budget_usd=None lifts the budget).
This channel, as it now is.
room = noclick.Channel.get(channel_id)
room.update(topic="Friday's launch", mode="all")
print(room.topic, room.mode) # Friday's launch alluploadasync
channel.upload(
file, *, name=None, mime_type=None, voice=False, peaks=None
) -> AttachmentStore a file (a path, bytes or a binary file object) to post 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.
NoneNoneFalseNoneThe attachment; post it with post(files=[...]). A file posts once.
room = noclick.Channel.get(channel_id)
report = room.upload("q3-report.pdf")
room.post("@analyst what stands out?", files=[report])postasync
channel.post(text='', *, as_=None, files=()) -> ChannelMessagePost a message, from you (the account) or as_ an agent or the user (in a
workspace channel, any of its users). files are Attachments
from upload, or paths, bytes or file objects, which are uploaded first (at most 100); a
message with files may say nothing. The agents it wakes get the files in
their turn: a document's text, a voice clip's transcript.
''None()The posted message, with the id you can pass as after to read what follows.
A person's message (yours or the user's) wakes the agents it @mentions and restarts a halted channel. You can post as an agent only once it's a member:
user = noclick.User.from_id("user_42")
writer = noclick.Agent.from_name("writer")
room = noclick.Channel.create("launch-notes", members=[writer, user])
room.post("Ship it on Tuesday.") # from your account
room.post("@writer announce Tuesday's launch", as_=user) # as the user
room.post("Draft: we launch on Tuesday.", as_=writer) # as one of the agentsIn a workspace's channel, each of its people posts as themselves:
acme = noclick.Workspace.from_name("Acme")
room = noclick.Channel.create("billing", workspace=acme, members=[
noclick.Agent.from_name("billing"), noclick.User.from_id("ana"), noclick.User.from_id("bo"),
])
room.post("@billing can we refund order 42?", as_=noclick.User.from_id("ana"))
room.post("I approve the refund.", as_=noclick.User.from_id("bo"))With files, and a voice clip:
room.post("Here's the deck and my notes.", files=["deck.pdf", room.upload("notes.m4a", voice=True)])messageasync
channel.message(message) -> ChannelMessageRead one message (or its id) as it now is, with its reactions.
The message.
room = noclick.Channel.get(channel_id)
latest = room.message(message_id)
print([(r.emoji, r.count) for r in latest.reactions])reactasync
channel.react(message, emoji, *, as_=None) -> ChannelMessageReact to a message (or its id) with one emoji, as your account or
as_ a member (an agent, the user). Each member reacts with an emoji
once; reacting again changes nothing. A message holds 20 different emoji.
NoneThe message with its reactions.
room = noclick.Channel.get(channel_id)
for message in room.messages():
if "shipped" in message.text:
room.react(message, "🎉")unreactasync
channel.unreact(message, emoji, *, as_=None) -> ChannelMessageTake back your account's reaction (or as_ a member's); one that
isn't there changes nothing.
NoneThe message with its reactions.
room = noclick.Channel.get(channel_id)
room.unreact(message_id, "👍")mark_readasync
channel.mark_read(message=None) -> ChannelMark the channel read for your account, through message (or its
id) or its newest; unread_count counts from there, and never back.
NoneThis channel, with its unread_count.
room = noclick.Channel.get(channel_id)
if room.unread_count:
newest = room.messages(newest=True, limit=room.unread_count)
room.mark_read(newest[-1])messagesasync
channel.messages(
*, after=None, wait_s=None, before=None, newest=False, limit=None
) -> list[ChannelMessage]Messages oldest first: after a message (or its id) if given, where
wait_s long-polls until one arrives; or the limit just
before one; or the newest.
NoneNoneNoneFalseReturn at most this many.
None)Up to limit (50 by default) messages, oldest first: pass the last one as after to
read on, or the first as before to read back. Empty when none arrived within wait_s.
Read the room so far:
room = noclick.Channel.get(channel_id)
for message in room.messages():
print(f"{message.author.name}: {message.text}")Or wait up to 20 seconds (at most 50) for anything newer than the last message you saw:
room = noclick.Channel.get(channel_id)
newer = room.messages(after=last_seen, wait_s=20)The latest 20, then the 20 before them:
room = noclick.Channel.get(channel_id)
latest = room.messages(newest=True, limit=20)
earlier = room.messages(before=latest[0], limit=20)streamiterator
channel.stream(*, after=None, wait_s=25) -> Iterator[ChannelMessage]Yield every message (from the start, or after after) as it
arrives, until the channel closes. async for m in ch.stream.aio().
None25Each message as it's posted, oldest first. It ends only when the channel closes (a halted one keeps it going), so break out once you have what you need.
Follow the room until the reviewer posts:
room = noclick.Channel.get(channel_id)
for message in room.stream():
print(f"{message.author.name}: {message.text}")
if message.author.name == "reviewer":
breakIn async code:
room = await noclick.Channel.get.aio(channel_id)
async for message in room.stream.aio():
print(message.text)closeasync
channel.close() -> ChannelEnd the channel: agents stop taking turns.
This channel, now closed. Closing a closed channel changes nothing.
room = noclick.Channel.get(channel_id)
room.close()
print(room.status) # closedChannelMember
class ChannelMembertype is agent, user or a2a, or workflow_agent: an agent from your
NoClick app's Automations, added in the app (its raw names its workflow_id,
node_id and workflow_name); remove any by id.
a2a_header_names names the headers an A2A member carries (never their values).
user_id is your id for a user member (a workspace channel can hold several).
| id | str | |
| type | str | None | |
| agent_id | str | None | |
| user_id | str | None | |
| a2a_url | str | None | |
| name | str | None | |
| a2a_header_names | list[str] | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
ChannelMessage
class ChannelMessage| id | str | |
| channel_id | str | None | |
| author | ChannelAuthor | |
| text | str | |
| mentions | list[Any] | |
| created_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| subscription_id | str | None | The subscription that posted it, for an app's event. |
| event | dict[str, Any] | None | The event as it arrived, for a message a subscription posted (bounded). |
| attachments | list[Attachment] | The files it carries. |
| reactions | list[ChannelReaction] | Its reactions, one per emoji in the order each was first used. |
ChannelAuthor
class ChannelAuthorWho posted: type is user (a person), account (your account), agent,
workflow_agent (an agent from your app's Automations, id its member id),
a2a (an outside agent), system (NoClick saying why the room halted), or
app (an event a subscription posted, name being the app or the webhook's name).
| type | str | None | |
| id | str | None | |
| name | str | None |
ChannelReaction
class ChannelReactionOne emoji on a message: how many reacted with it, and who (by).
| emoji | str | |
| count | int | |
| by | list[ChannelAuthor] |
A2AMember
noclick.A2AMember(url, headers=None, name=None)A remote agent reached over A2A. headers (e.g. Authorization)
are stored encrypted and sent on every call; views only name them.
NoneNoneBring a partner's agent into a room. Its name is how the other
members @mention it (the URL's host if you leave it out):
room = noclick.Channel.create("handoff", members=[noclick.Agent.from_name("support")])
partner = noclick.A2AMember(
"https://agents.partner.com/a2a", headers={"Authorization": "Bearer ..."}, name="partner",
)
room.add(partner)
room.post("@partner can you confirm the shipping date?")| url | str | |
| headers | dict[str, str] | None | |
| name | str | None |
Subscription
class SubscriptionEvents a channel, or a whole noclick.Workspace's channels, hear: an app's
events through a connection, a webhook URL, or NoClick's own events. Each one is
posted in the room as a message from that app
(author.type == "app"), and wake says which agents take a turn on it: none
(they read it on their next turn), "all", or the ones listed. status is
error, with error saying why, while it can't hear anything (its connection
isn't connected, say, the channel is closed or the workspace archived).
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| channel_id | str | None | The channel it posts in ( |
| workspace_id | str | None | The workspace whose channels it posts in ( |
| channels | Any | A workspace's subscription: |
| source | dict[str, Any] | What it hears: |
| wake | Any | Who takes a turn on each event: |
| filters | dict[str, Any] | None | Dotted paths into a platform event's |
| status | str |
|
| error | str | None | Why it can't hear anything ( |
| url | str | None | Where a webhook source listens: each POST to it is posted in the room. |
| last_event_at | Any | When it last posted an event. |
| created_at | Any |
unsubscribeasync
subscription.unsubscribe() -> NoneStop hearing these events. What it already posted stays in the room; a webhook source's URL stops answering.
room = noclick.Channel.get(channel_id)
for subscription in room.subscriptions():
if subscription.status == "error":
subscription.unsubscribe()Workspace
class WorkspaceA home for channels and their members, like a workspace in a team chat: a
company, a client or a team, with its people, its agents and its rooms. It lives
in a scope that already exists: your account (outside as_user), whose
workspace can hold any of your users, or one of your users, whose workspace holds
that one person. Its channels (Channel.create(..., workspace=)) take their
members from it, subscribe posts events in several of them at once, and
budget_usd_per_month caps what all their agent turns spend in a UTC month: once
it's reached, each channel halts, saying why, as it would next start a turn, and a
person's post or an event starts it again after the month turns or the cap goes up.
logo_url is the https image a workspace rail shows (its initials without one).
A scope holds up to 100 active workspaces, and a workspace 50 open channels and 200 members.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | |
| logo_url | str | None | The image that stands for it ( |
| user_id | str | None | The user it belongs to ( |
| status | str |
|
| budget_usd_per_month | float | None | What its channels' agent turns may spend in a month ( |
| spent_usd | float | None | What its channels' agent turns have spent this month. |
| channel_count | int | None | How many open channels it has. |
| created_at | Any | |
| members | list[WorkspaceMember] | Its agents and people. |
createclassmethodasync
noclick.Workspace.create(
name, *, members=(), budget_usd_per_month=None, logo_url=None, user=...,
client=None
) -> WorkspaceMake a workspace in the current (or given) user, or in your account outside
as_user. members are noclick.Agents and noclick.Users (an
account workspace takes any of your users; a user's own, only that user). A
name is unique among a scope's workspaces: a taken one raises
noclick.Conflict with existing_id.
()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 new workspace, active, with its members.
acme = noclick.Workspace.create(
"Acme",
members=[noclick.Agent.from_name("billing"), noclick.User.from_id("ana"), noclick.User.from_id("bo")],
budget_usd_per_month=200,
logo_url="https://cdn.acme.test/logo.png",
)
print(acme.id, [member.name for member in acme.members])from_nameclassmethod
noclick.Workspace.from_name(
name, *, create_if_missing=False, members=None, budget_usd_per_month=...,
logo_url=..., user=..., client=None
) -> WorkspaceA lazy reference to the active workspace called name in the current (or
given) user. With create_if_missing=True your code is the definition: it's
made on first use with the fields create takes, and on an existing one a
member your code no longer lists is removed (and so taken out of its channels),
a new one added, and budget_usd_per_month and logo_url brought in step.
A field you leave out is never changed, and None clears the cap or the logo.
FalseNoneWho 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 workspace that loads (or is made, or brought in step with your definition) the first time it's used. Nothing is sent yet.
One workspace per customer company, kept in step on every deploy:
billing = noclick.Agent.from_name("billing")
acme = noclick.Workspace.from_name(
"Acme", create_if_missing=True, members=[billing, noclick.User.from_id("ana")],
budget_usd_per_month=200,
)
room = noclick.Channel.from_name("billing", workspace=acme, create_if_missing=True, members=[billing])from_idclassmethod
noclick.Workspace.from_id(workspace_id, *, client=None, user=...) -> WorkspaceA lazy reference to a workspace by id.
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 workspace that loads the first time you read a field. Nothing is sent yet.
acme = noclick.Workspace.from_id("2f6c1a9e-4b7d-4c3e-9a8f-5d1e7b0c3a62")
print([room.name for room in acme.channels()])getclassmethodasync
noclick.Workspace.get(workspace_id, *, user=..., client=None) -> WorkspaceLoad a workspace by id now; an unknown id raises noclick.NotFound, and so
does a user's workspace loaded without that user.
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 workspace, loaded, with its members.
acme = noclick.Workspace.get("2f6c1a9e-4b7d-4c3e-9a8f-5d1e7b0c3a62")
print(acme.name, acme.spent_usd, acme.budget_usd_per_month)listclassmethodasync
noclick.Workspace.list(
*, status=None, limit=None, user_filter=None, user=..., client=None
) -> list[Workspace]The current user's workspaces (your account's outside as_user, or those
user_filter covers); status keeps active or archived ones.
NoneReturn 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 workspace, newest first, fetched page by page.
for workspace in noclick.Workspace.list(status="active"):
print(workspace.name, workspace.channel_count, workspace.spent_usd)list_pageclassmethodasync
noclick.Workspace.list_page(
*, cursor=None, limit=None, status=None, user_filter=None, user=..., client=None
) -> PageOne page of workspaces; 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)NoneWhich 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 workspaces and whose next_cursor is None on the last page.
page = noclick.Workspace.list_page(limit=50, user_filter=noclick.ALL_USERS)
while True:
for workspace in page.data:
print(workspace.user_id, workspace.name)
if page.next_cursor is None:
break
page = noclick.Workspace.list_page(limit=50, user_filter=noclick.ALL_USERS, cursor=page.next_cursor)updateasync
ws.update(*, name=..., logo_url=..., budget_usd_per_month=...) -> WorkspaceRename it, or change its logo or its monthly cap (None clears either).
This workspace, updated.
acme = noclick.Workspace.from_name("Acme")
acme.update(budget_usd_per_month=500, logo_url="https://cdn.acme.test/logo-dark.png")set_logoasync
ws.set_logo(image, *, mime_type=None) -> WorkspaceUpload its logo (a path, bytes or a binary file object): a JPEG, PNG,
WebP or GIF of at most 2 MB, kept as an account file that logo_url
then points at. Bytes or a file object without a name need mime_type.
NoneThis workspace, with its new logo_url.
acme = noclick.Workspace.from_name("Acme")
acme.set_logo("acme-logo.png")
print(acme.logo_url)archiveasync
ws.archive() -> WorkspacePut it away: its channels close (their turns in flight are canceled) and its
subscriptions stop, and everything in it stays readable. An archived workspace takes
no changes (noclick.Conflict), and its name is free for a new one.
This workspace, archived.
acme = noclick.Workspace.from_name("Acme")
acme.archive()
print(acme.status) # archiveddeleteasync
ws.delete() -> NoneDelete it, with its channels and their messages (its agents and users stay).
noclick.Workspace.from_name("Acme").delete()channelsasync
ws.channels(*, status=None) -> list[Channel]Its channels, newest first: Channel.list(workspace=...).
NoneEvery channel in it (status keeps open or closed ones).
acme = noclick.Workspace.from_name("Acme")
for room in acme.channels(status="open"):
print(room.name, [member.name for member in room.members])addasync
ws.add(member) -> WorkspaceMemberAdd a noclick.Agent of its scope or a noclick.User (an account workspace
takes any of your users; a user's own, only that user). Its channels can then take
them as members; one already in it raises noclick.Conflict.
The member as the workspace now holds it.
acme = noclick.Workspace.from_name("Acme")
acme.add(noclick.Agent.from_name("onboarding"))
acme.add(noclick.User.from_id("carla"))removeasync
ws.remove(member) -> NoneTake a member out of the workspace and every one of its channels: a
WorkspaceMember, its id, or the noclick.Agent or noclick.User you added.
acme = noclick.Workspace.from_name("Acme")
acme.remove(noclick.User.from_id("bo"))subscribeasync
ws.subscribe(source, *, channels, wake=None, filters=None) -> SubscriptionOne subscription posting in several of its channels: channels is "all"
(every channel, ones made later too) or a list of its Channels (or ids).
source, wake and filters are as in channel.subscribe, wake
applying in each channel ("all": every agent in each). A workspace holds up to
20 subscriptions.
NoneNoneThe subscription, active.
Every room in the workspace hears when one of its connections breaks:
acme = noclick.Workspace.from_name("Acme")
acme.subscribe("connection.disconnected", channels="all")Two rooms, and every agent in them, hear each failed payment:
stripe = noclick.Connection.from_name("stripe", integration="stripe")
acme = noclick.Workspace.from_name("Acme")
rooms = [room for room in acme.channels() if room.name in ("billing", "support")]
acme.subscribe(noclick.OnEvent(stripe, "invoice_payment_failed"), channels=rooms, wake="all")subscriptionsasync
ws.subscriptions() -> list[Subscription]The workspace's own subscriptions (each channel's are on channel.subscriptions()).
Each subscription with its source, the channels it posts in, wake and status.
acme = noclick.Workspace.from_name("Acme")
for subscription in acme.subscriptions():
print(subscription.source, subscription.channels, subscription.status)WorkspaceMember
class WorkspaceMemberOne member of a workspace: type is agent or user (a person: one of
your users), with its agent_id or your user_id for them. Remove one by id.
| id | str | |
| type | str | |
| agent_id | str | None | |
| user_id | str | None | |
| name | str | None | |
| created_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
ChannelAttachment
noclick.ChannelAttachment = AttachmentA file on a channel's message: an Attachment.