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 Channel

A conversation between members. with closes it on exit.

Attributes
idstr
namestr
topicstr | None

One line on what the channel is for.

modestr | None
statusstr | None
halt_reasonstr | None

Why a halted channel stopped (it wakes when a person posts).

unread_countint | None

Messages since your account last read it (mark_read; its own posts count as read).

max_agent_turnsint | None
budget_usdfloat | None
user_idstr | None
workspace_idstr | None

The noclick.Workspace it belongs to (None for one on its own).

created_atAny
memberslist[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
) -> Channel

Open 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).

Parameters
namestrrequired
membersSequence[Any]default ()
modestr | Nonedefault None
max_agent_turnsint | Nonedefault None
max_mention_hopsint | Nonedefault None
budget_usdfloat | Nonedefault None
workspaceAnydefault None
topicstr | 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
Channel

The new channel, open, with its members.

Usage

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
) -> Channel

A 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.

Parameters
namestrrequired
create_if_missingbooldefault False
membersSequence[Any] | Nonedefault None
modestr | Nonedefault None
topicstr | Nonedefault None
max_agent_turnsAnyomit to leave it unchanged
max_mention_hopsAnyomit to leave it unchanged
budget_usdAnyomit to leave it unchanged
triggersSequence[Any] | Nonedefault None
workspaceAnydefault 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
Channel

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

Usage

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.

Parameters
statusstr | Nonedefault None
workspaceAnydefault 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[Channel]

Every matching channel, newest first, fetched page by page. status="open" also takes halted channels, which wake when a person posts.

Usage

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
) -> Page

One page of channels; pass next_cursor back as cursor. workspace is as in list.

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)
statusstr | Nonedefault None
workspaceAnydefault 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 channels and whose next_cursor is None on the last page.

Usage
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=...) -> Channel

A lazy reference to a channel by id.

Parameters
channel_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
Channel

A channel that loads the first time you read a field. Nothing is sent yet.

Usage
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) -> Channel

Load 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).

Parameters
channel_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
Channel

The channel, loaded, with its members.

Usage

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.

Returns
list[AgentTrigger]

Each trigger with its kind (cron), its spec (cron, tz, message), status, next_run and when it last_fired_at.

Usage
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) -> AgentTrigger

Post 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.

Parameters
triggerCronrequired
Returns
AgentTrigger

The trigger as added, with its next_run.

Usage
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) -> None

Remove a schedule: an AgentTrigger (or its id), or the Cron you added (noclick.NotFound if the channel has none like it).

Parameters
triggerAnyrequired
Usage
room = noclick.Channel.from_name("launch-notes")
for trigger in room.triggers():
    room.remove_trigger(trigger)

subscribeasync

channel.subscribe(source, *, wake=None, filters=None) -> Subscription

Post 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.

Parameters
sourceAnyrequired
wakeAnydefault None
filtersdict[str, Any] | Nonedefault None
Returns
Subscription

The subscription, active (a webhook source with its url).

Usage

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 here

A 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()).

Returns
list[Subscription]

Each subscription with its source, wake, status and, for a webhook, its url.

Usage
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) -> ChannelMember

Add 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.

Parameters
memberAnyrequired
Returns
ChannelMember

The member as the channel now holds it; remove it later by its id.

Usage
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) -> None

Remove a member (a ChannelMember, its id, or the Agent / user / URL you added).

Parameters
memberAnyrequired
Usage
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=...
) -> Channel

Change an open channel's topic, mode or hard stops; what you leave out stays (budget_usd=None lifts the budget).

Parameters
topicAnyomit to leave it unchanged
modeAnyomit to leave it unchanged
max_agent_turnsAnyomit to leave it unchanged
max_mention_hopsAnyomit to leave it unchanged
budget_usdAnyomit to leave it unchanged
Returns
Channel

This channel, as it now is.

Usage
room = noclick.Channel.get(channel_id)
room.update(topic="Friday's launch", mode="all")
print(room.topic, room.mode)   # Friday's launch all

uploadasync

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

Store 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.

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

The attachment; post it with post(files=[...]). A file posts once.

Usage
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=()) -> ChannelMessage

Post 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.

Parameters
textstrdefault ''
as_Anydefault None
filesSequence[Any]default ()
Returns
ChannelMessage

The posted message, with the id you can pass as after to read what follows.

Usage

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 agents

In 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) -> ChannelMessage

Read one message (or its id) as it now is, with its reactions.

Parameters
messageAnyrequired
Returns
ChannelMessage

The message.

Usage
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) -> ChannelMessage

React 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.

Parameters
messageAnyrequired
emojistrrequired
as_Anydefault None
Returns
ChannelMessage

The message with its reactions.

Usage
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) -> ChannelMessage

Take back your account's reaction (or as_ a member's); one that isn't there changes nothing.

Parameters
messageAnyrequired
emojistrrequired
as_Anydefault None
Returns
ChannelMessage

The message with its reactions.

Usage
room = noclick.Channel.get(channel_id)
room.unreact(message_id, "👍")

mark_readasync

channel.mark_read(message=None) -> Channel

Mark the channel read for your account, through message (or its id) or its newest; unread_count counts from there, and never back.

Parameters
messageAnydefault None
Returns
Channel

This channel, with its unread_count.

Usage
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.

Parameters
afterAnydefault None
wait_sint | Nonedefault None
beforeAnydefault None
newestbooldefault False
limitint | None

Return at most this many.

(default None)
Returns
list[ChannelMessage]

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.

Usage

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().

Parameters
afterAnydefault None
wait_sintdefault 25
Returns
Iterator[ChannelMessage]

Each 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.

Usage

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":
        break

In async code:

room = await noclick.Channel.get.aio(channel_id)
async for message in room.stream.aio():
    print(message.text)

closeasync

channel.close() -> Channel

End the channel: agents stop taking turns.

Returns
Channel

This channel, now closed. Closing a closed channel changes nothing.

Usage
room = noclick.Channel.get(channel_id)
room.close()
print(room.status)   # closed

ChannelMember

class ChannelMember

type 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).

Attributes
idstr
typestr | None
agent_idstr | None
user_idstr | None
a2a_urlstr | None
namestr | None
a2a_header_nameslist[str]
rawdict[str, Any]

The response JSON exactly as the API sent it.

ChannelMessage

class ChannelMessage
Attributes
idstr
channel_idstr | None
authorChannelAuthor
textstr
mentionslist[Any]
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

subscription_idstr | None

The subscription that posted it, for an app's event.

eventdict[str, Any] | None

The event as it arrived, for a message a subscription posted (bounded).

attachmentslist[Attachment]

The files it carries.

reactionslist[ChannelReaction]

Its reactions, one per emoji in the order each was first used.

ChannelAuthor

class ChannelAuthor

Who 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).

Attributes
typestr | None
idstr | None
namestr | None

ChannelReaction

class ChannelReaction

One emoji on a message: how many reacted with it, and who (by).

Attributes
emojistr
countint
bylist[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.

Parameters
urlstrrequired
headersdict[str, str] | Nonedefault None
namestr | Nonedefault None
Usage

Bring 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?")
Attributes
urlstr
headersdict[str, str] | None
namestr | None

Subscription

class Subscription

Events 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).

Attributes
idstr
channel_idstr | None

The channel it posts in (None for a workspace's).

workspace_idstr | None

The workspace whose channels it posts in (None for a channel's).

channelsAny

A workspace's subscription: "all" (every channel, later ones too) or the channel ids it posts in.

sourcedict[str, Any]

What it hears: {kind: "event", connection, event, filters, message}, {kind: "webhook", name, message} or {kind: "platform", type, message}.

wakeAny

Who takes a turn on each event: None (no one), "all" or a list of agent ids.

filtersdict[str, Any] | None

Dotted paths into a platform event's data or a webhook's JSON body, and the values they must have.

statusstr

active, or error while it can't hear anything.

errorstr | None

Why it can't hear anything (None while active).

urlstr | None

Where a webhook source listens: each POST to it is posted in the room.

last_event_atAny

When it last posted an event.

created_atAny

unsubscribeasync

subscription.unsubscribe() -> None

Stop hearing these events. What it already posted stays in the room; a webhook source's URL stops answering.

Usage
room = noclick.Channel.get(channel_id)
for subscription in room.subscriptions():
    if subscription.status == "error":
        subscription.unsubscribe()

Workspace

class Workspace

A 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.

Attributes
idstr
namestr
logo_urlstr | None

The image that stands for it (None: its initials).

user_idstr | None

The user it belongs to (None: your account).

statusstr

active, or archived: its channels closed, everything still readable.

budget_usd_per_monthfloat | None

What its channels' agent turns may spend in a month (None: no cap of its own).

spent_usdfloat | None

What its channels' agent turns have spent this month.

channel_countint | None

How many open channels it has.

created_atAny
memberslist[WorkspaceMember]

Its agents and people.

createclassmethodasync

noclick.Workspace.create(
    name, *, members=(), budget_usd_per_month=None, logo_url=None, user=...,
    client=None
) -> Workspace

Make 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.

Parameters
namestrrequired
membersSequence[Any]default ()
budget_usd_per_monthfloat | Nonedefault None
logo_urlstr | 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
Workspace

The new workspace, active, with its members.

Usage
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
) -> Workspace

A 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.

Parameters
namestrrequired
create_if_missingbooldefault False
membersSequence[Any] | Nonedefault None
budget_usd_per_monthAnyomit to leave it unchanged
logo_urlAnyomit to leave it unchanged
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
Workspace

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

Usage

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=...) -> Workspace

A lazy reference to a workspace by id.

Parameters
workspace_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
Workspace

A workspace that loads the first time you read a field. Nothing is sent yet.

Usage
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) -> Workspace

Load a workspace by id now; an unknown id raises noclick.NotFound, and so does a user's workspace loaded without that user.

Parameters
workspace_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
Workspace

The workspace, loaded, with its members.

Usage
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.

Parameters
statusstr | 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[Workspace]

Every matching workspace, newest first, fetched page by page.

Usage
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
) -> Page

One page of workspaces; 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)
statusstr | 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 workspaces and whose next_cursor is None on the last page.

Usage
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=...) -> Workspace

Rename it, or change its logo or its monthly cap (None clears either).

Parameters
nameAnyomit to leave it unchanged
logo_urlAnyomit to leave it unchanged
budget_usd_per_monthAnyomit to leave it unchanged
Returns
Workspace

This workspace, updated.

Usage
acme = noclick.Workspace.from_name("Acme")
acme.update(budget_usd_per_month=500, logo_url="https://cdn.acme.test/logo-dark.png")

archiveasync

ws.archive() -> Workspace

Put 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.

Returns
Workspace

This workspace, archived.

Usage
acme = noclick.Workspace.from_name("Acme")
acme.archive()
print(acme.status)   # archived

deleteasync

ws.delete() -> None

Delete it, with its channels and their messages (its agents and users stay).

Usage
noclick.Workspace.from_name("Acme").delete()

channelsasync

ws.channels(*, status=None) -> list[Channel]

Its channels, newest first: Channel.list(workspace=...).

Parameters
statusstr | Nonedefault None
Returns
list[Channel]

Every channel in it (status keeps open or closed ones).

Usage
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) -> WorkspaceMember

Add 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.

Parameters
memberAnyrequired
Returns
WorkspaceMember

The member as the workspace now holds it.

Usage
acme = noclick.Workspace.from_name("Acme")
acme.add(noclick.Agent.from_name("onboarding"))
acme.add(noclick.User.from_id("carla"))

removeasync

ws.remove(member) -> None

Take 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.

Parameters
memberAnyrequired
Usage
acme = noclick.Workspace.from_name("Acme")
acme.remove(noclick.User.from_id("bo"))

subscribeasync

ws.subscribe(source, *, channels, wake=None, filters=None) -> Subscription

One 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.

Parameters
sourceAnyrequired
channelsAnyrequired
wakeAnydefault None
filtersdict[str, Any] | Nonedefault None
Returns
Subscription

The subscription, active.

Usage

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()).

Returns
list[Subscription]

Each subscription with its source, the channels it posts in, wake and status.

Usage
acme = noclick.Workspace.from_name("Acme")
for subscription in acme.subscriptions():
    print(subscription.source, subscription.channels, subscription.status)

WorkspaceMember

class WorkspaceMember

One 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.

Attributes
idstr
typestr
agent_idstr | None
user_idstr | None
namestr | None
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

ChannelAttachment

noclick.ChannelAttachment = Attachment

A file on a channel's message: an Attachment.