Triggers

Triggers: what wakes an agent besides a message.

fulfilment = noclick.Agent.from_name(
    "fulfilment", create_if_missing=True, instructions="You confirm new orders with the customer.",
    triggers=[noclick.OnWebhook(name="orders", message="A new order arrived.")],
)

An OnWebhook gives it a URL whose POST body is the turn, an OnEvent subscribes it to a connection's app events, an OnPlatformEvent to NoClick's own events (an operation failing, a connection breaking), and a Cron wakes it on a schedule (an agent with one is an automation), and an OnQueue makes each message on a queue a turn. Mail and calls wake it too, set up where they live (Inbox(agent=), PhoneNumber.answer_with); triggers() lists them as kinds email and call, and while its policy may hold one of its tools for a person, as kind approval: an answered approval wakes it in the held call's conversation.

A webhook, app event or queue trigger takes conversation=, a template over what arrived ("issue-{issue.number}"): deliveries that render the same key continue one conversation, and the turn's operation records the key as input["conversation"].

Cron

noclick.Cron(expr, tz='UTC', message=None)

A schedule: a five-field cron expression (minute, hour, day of month, month, day of week: numbers, *, ranges, steps, lists and month or day names) read in a timezone, so "0 9 * * 1-5" with tz="America/New_York" means 9am New York time on weekdays, daylight saving included. When both day fields are restricted a run must match both. An agent's schedule wakes the agent with message as the turn (each run a conversation of its own); a wake-up's every wakes one conversation at each time, with its note (a routine: agent.wake); a channel's posts message in the room; an app's (invite-only beta) calls one of its paths. tz="user" reads it in a user's agent's (or channel's) user's timezone, following them when they move (UTC while they have none; an account agent refuses it, having no user); a wake-up reads it in the timezone of the user whose conversation it wakes.

Every agent turn is told the date and time, so a schedule's message can say "last month" or "this week" and mean it.

Parameters
exprstrrequired
tzstr

An IANA timezone, or "user": the agent's user's timezone, followed when they move.

(default 'UTC')
messagestr | None

The turn's text when this schedule wakes an agent; apps ignore it.

(default None)
Usage

Wake an agent every weekday morning:

digest = noclick.Agent.from_name(
    "digest", create_if_missing=True, instructions="You summarize the overnight tickets.",
    triggers=[noclick.Cron("0 8 * * 1-5", tz="Europe/London", message="Post the overnight summary")],
)

Call an app's path every hour (invite-only beta):

app = noclick.Project.from_name("booking")
app.schedule(noclick.Cron("0 * * * *"), path="/jobs/reminders")

Brief each user at 8am where they are:

with noclick.as_user("user_42"):
    assistant = noclick.Agent.from_name("assistant")
    assistant.add_trigger(noclick.Cron("0 8 * * *", tz="user", message="Brief me on today's calendar"))
Attributes
exprstr
tzstr

An IANA timezone, or "user": the agent's user's timezone, followed when they move.

messagestr | None

The turn's text when this schedule wakes an agent; apps ignore it.

OnWebhook

noclick.OnWebhook(name, message=None, conversation=None)

A URL that wakes the agent: each POST to it is a turn, its body the message (a JSON body is passed as JSON). The URL is AgentTrigger.url; anyone who has it can wake the agent, so keep it like a key. It stays the same while a trigger of that name stays on the agent. message is what the agent is told with every delivery, above the body.

Parameters
namestrrequired
messagestr | None

Standing instructions sent with each delivery, above its body. A channel can subscribe to one too (channel.subscribe(noclick.OnWebhook(...))), its POSTs posted in the room.

(default None)
conversationstr | None

The conversation a delivery continues: a template over its body, "order-{order.id}"; a NoClick-Conversation header wins.

(default None)
Usage

Let your order system wake an agent:

fulfilment = noclick.Agent.from_name(
    "fulfilment", create_if_missing=True, instructions="You confirm new orders with the customer.",
    triggers=[noclick.OnWebhook(name="orders", message="A new order arrived. Confirm it with the customer.")],
)
[orders] = fulfilment.triggers()
print(orders.url)   # POST your order JSON here

Keep each order in one conversation, keyed by a field of the body:

fulfilment = noclick.Agent.from_name("fulfilment")
fulfilment.add_trigger(noclick.OnWebhook(name="order-updates", conversation="order-{order.id}"))
Attributes
namestr
messagestr | None

Standing instructions sent with each delivery, above its body. A channel can subscribe to one too (channel.subscribe(noclick.OnWebhook(...))), its POSTs posted in the room.

conversationstr | None

The conversation a delivery continues: a template over its body, "order-{order.id}"; a NoClick-Conversation header wins.

OnEvent

noclick.OnEvent(connection, event, *, message=None, conversation=None, **filters)

An app event from one of the agent's connections, such as a Slack message or a new HubSpot contact. connection is a Connection or its name, the agent's own user's; event names one of the integration's app events (channel_message for Slack, contact_created for HubSpot; list them with connection.events()) and each keyword argument narrows it the way the integration takes it, like channel="C0123ABCD" (a Slack channel's id: slack.lookup("channel") lists them). message is what the agent is told with every event, above it. A thread (a Slack thread) stays one conversation, so the agent's replies land in it. conversation keys conversations yourself, a template over the event as the app sent it: "issue-{issue.number}" keeps everything about one issue in one. A channel subscribes to the same events (channel.subscribe(noclick.OnEvent(...))), each posted in the room. The connection needn't be connected yet: the trigger is error until it is.

Parameters
connectionAnyrequired
eventstrrequired
messagestr | Nonedefault None
conversationstr | Nonedefault None
**filtersAny
Usage

Answer every message in a Slack channel:

slack = noclick.Connection.from_name("slack", integration="slack", create_if_missing=True)
support = noclick.Agent.from_name(
    "support", create_if_missing=True, instructions="You answer questions in #support.",
    tools=[slack],
    triggers=[noclick.OnEvent(slack, "channel_message", channel="C0123ABCD")],
)

Or wake a sales agent for each new contact:

sales = noclick.Agent.from_name("sales")
sales.add_trigger(noclick.OnEvent("hubspot", "contact_created", message="Research this contact and draft a first email."))

Pick up GitHub issues when they're labelled, one conversation per issue:

github = noclick.Connection.from_name("github", integration="github", create_if_missing=True)
triage = noclick.Agent.from_name("triage")
triage.add_trigger(noclick.OnEvent(github, "issue_labeled", repository="acme/api", conversation="issue-{issue.number}"))
Attributes
connectionAny
eventstr
filtersdict[str, Any]
messagestr | None
conversationstr | None
connectionAny
eventstr
filters
messagestr | None
conversationstr | None

OnPlatformEvent

noclick.OnPlatformEvent(type, filters=None, message=None, conversation=None)

One of NoClick's own events (the types noclick.Event.types() lists, as your webhooks receive them): an operation failing, a connection breaking, a channel halting, a workflow's run failing, an agent built in the app asking for you, a payment going through. type names one, and * matches the rest of a name ("operation.*"); filters are dotted paths into the event's data and the values they must have. message is what the agent is told with every event, above it; conversation keys conversations by a template over the event ("connection-{data.id}"), else each event is a conversation of its own.

The agent hears its user's events, and an agent of your account's hears every event, your users' included. It never hears what its own turns caused (its turn finishing), and a chain of agents waking each other this way stops five turns deep. A channel subscribes to the same events (channel.subscribe(noclick.OnPlatformEvent(...))).

Parameters
typestrrequired
filtersdict[str, Any] | Nonedefault None
messagestr | Nonedefault None
conversationstr | Nonedefault None
Usage

Have an agent look into every failed job of your coding agents:

triage = noclick.Agent.from_name(
    "triage", create_if_missing=True, instructions="You find out why work failed and say what to do.",
    triggers=[noclick.OnPlatformEvent("operation.failed", filters={"kind": "agent.task"},
                                      message="A job failed. Find out why.")],
)

One conversation per broken connection:

support = noclick.Agent.from_name("support")
support.add_trigger(noclick.OnPlatformEvent("connection.disconnected", conversation="connection-{data.id}",
                                            message="Tell the user how to reconnect it."))

Watch over the workflows built in the app, one conversation per workflow:

ops = noclick.Agent.from_name("ops")
ops.add_trigger(noclick.OnPlatformEvent("workflow.run.failed", conversation="workflow-{data.workflow_id}",
                                        message="A workflow failed. Find out why; pause it if it's useless."))
Attributes
typestr
filtersdict[str, Any] | None
messagestr | None
conversationstr | None

OnQueue

noclick.OnQueue(queue, *, message=None, batch_size=1, concurrency=1, conversation=None)

Each message on a queue becomes a turn: the agent reads message (its standing instructions, if any) and then the message's JSON body. With batch_size above 1 (up to 25), one turn takes up to that many waiting messages at once, as a JSON list, for work that's cheaper in bulk. queue is a Queue or its name, the agent's own user's.

Delivery follows the queue's rules: a turn that finishes acks its messages; a turn that fails hands them back for another try (after 30 seconds per attempt they've had), and a message that fails the queue's max_attempts times becomes a dead letter, the turn's error its last_error. Each turn is a conversation of its own, unless conversation keys it from the message (one message a turn only; turns of one conversation take their turn one at a time). concurrency (1 to 50) runs up to that many turns at once. While the agent is paused, messages wait.

When the agent's budget, its user's cap or your account's funds run out, the trigger pauses instead of failing turns: messages wait with their attempts unchanged, the trigger's status is error saying why, and it carries on by itself once there's money to spend again (it looks every half minute).

Parameters
queueAnyrequired
messagestr | Nonedefault None
batch_sizeintdefault 1
concurrencyintdefault 1
conversationstr | Nonedefault None
Usage

Let an agent work through a queue your app fills:

invoices = noclick.Queue.from_name("invoices", create_if_missing=True)
support = noclick.Agent.from_name(
    "support-bot", create_if_missing=True, instructions="You chase unpaid invoices by email.",
    tools=["gmail.send_email", "stripe.list_invoices"],
    triggers=[noclick.OnQueue(invoices, message="Chase this unpaid invoice.")],
)
invoices.put({"invoice": "INV-1042", "customer": "acme"})

Ten messages a turn, by the queue's name:

digest = noclick.Agent.from_name("digest")
digest.add_trigger(noclick.OnQueue("webhook-buffer", batch_size=10, message="Summarize these events."))

Five turns at once, each customer's messages in their own conversation:

enrich = noclick.Agent.from_name("enrich")
enrich.add_trigger(noclick.OnQueue("signups", concurrency=5, conversation="customer-{customer_id}"))
Attributes
queueAny
messagestr | None
batch_sizeint
concurrencyint
conversationstr | None
queueAny
messagestr | None
batch_sizeint
concurrencyint
conversationstr | None

AgentTrigger

class AgentTrigger

One thing that wakes an agent, as agent.triggers() reads it.

kind is cron, webhook, event, platform, queue, email, call or approval; spec is its definition (name for a webhook; connection, event and filters for an app event; type and filters for a NoClick event; message and conversation where they were given; inbox_id and address for email; phone_number_id and number for calls; cron, tz and message for a schedule; queue, batch_size and concurrency for a queue; tools, the ones policy may hold, for an approval). An approval trigger is there while your policy may hold one of its tools for a person: an answered approval wakes it in the held call's conversation. status is active, paused (agent.pause()) or error, with error saying why it can't fire (a connection that isn't connected, say, a queue's turns waiting for money, or its turns failing the same way five times in a row, which stops it until the agent is resumed).

Attributes
idstr
kindstr
specdict[str, Any]
statusstr
urlstr | None

The URL a webhook trigger listens at (None for other kinds).

next_runAny

When a schedule next wakes the agent (None for other kinds, and while paused or in error).

last_fired_atAny
errorstr | None
created_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

definedbool

Whether your code defines it (cron, webhook, event, platform, queue); email and call come from an inbox or a phone number and are changed there, and approval from your policy.