Triggers and automations
Wake an agent on a webhook, an app event, one of NoClick's own events, a schedule (as an automation) or each message on a queue.
A trigger wakes an agent without anyone messaging it: a call to a URL, something happening in one of its connections, or a time on a schedule. Each time one fires, the agent takes a turn, exactly as if it had been sent a message, and reads what arrived the way a trigger wired into an agent on the canvas does. An agent with a schedule is an automation, and the developer console lists automations apart from your other agents.
| Trigger | Wakes the agent | Defined with |
|---|---|---|
| Webhook | on each POST to a URL it's given | noclick.OnWebhook |
| App event | when something happens in a connection: a Slack message, a new HubSpot contact | noclick.OnEvent |
| NoClick event | when something happens on the platform: an operation fails, a connection breaks | noclick.OnPlatformEvent |
| on each message to an inbox it answers | Inbox.create(agent=...) | |
| Call | on each call to a phone number it answers | PhoneNumber.answer_with |
| Schedule | at the times a cron expression names | noclick.Cron |
| Queue | on each message put on a queue | noclick.OnQueue |
| Approval | when a person answers a tool call your policy held | your policy |
A trigger wakes the agent every time it fires. To give one conversation a single turn later, or when something finishes, set a wake-up instead.
Wake it with a webhook
fulfilment = noclick.Agent.from_name(
"fulfilment",
create_if_missing=True,
instructions="You confirm new orders with the customer by email.",
tools=["gmail.send_email_message"],
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 herePoint your order system, a form tool or any service that sends webhooks at url. Each POST is a turn: the agent reads message, then the call (its method, any query parameters, and the body: JSON as JSON, anything else as text, up to 256 KB). Headers never reach the agent, so a caller's credentials stay out of its turn. The call answers 202 at once with the delivery: its status (delivered, or pending while its conversation is still answering an earlier one), the turn's operation_id once it has one, and the conversation it runs in. The turn runs on.
Anyone with the URL can wake the agent, so keep it like a key. It stays the same for as long as a webhook trigger of that name stays on the agent; remove it and add it again for a new URL.
Every delivery starts a conversation of its own. To continue one instead, send its key in a NoClick-Conversation header, your ticket id say, or give the trigger a conversation template (Keep related deliveries together).
Wake it on an app event
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. Reply in the thread.",
tools=[slack],
triggers=[noclick.OnEvent(slack, "channel_message", channel="C0123ABCD")],
)OnEvent subscribes the agent to one of a connection's app events, named as its integration names them: channel_message or app_mention for Slack, contact_created for HubSpot, issue_created for Linear, issue_labeled for GitHub, invoice_payment_failed for Stripe. The connection is from the agent's own scope, its user's or your account's, named or passed as a Connection, and keyword arguments narrow the event, each as the integration's own field takes it: a Slack channel is the channel's id, which slack.lookup("channel") lists. message= works as it does for a webhook.
events() lists the app events a connection's integration has, with the filters each takes (one whose description starts Required. must be given). Like operations(), it reads the catalog, so the connection needn't be connected yet. Only what an app pushes as it happens is an event; new mail in Gmail, which NoClick polls for, isn't one:
for event in slack.events():
print(event.name, event.description, list(event.filters))The turn carries the event as the service described it, with the ids the agent needs to act on it: the channel and thread to reply in, the contact to update. A thread stays one conversation, so a follow-up in the same Slack thread continues the agent's conversation there. Messages the agent itself sends to Slack never wake it again.
An event trigger needs its connection connected. Until it is, the trigger's status is error and nothing fires; once the account connects (or reconnects), NoClick registers it and the next event wakes the agent. Deleting the connection stops the trigger, with error saying so.
A label is a good switch for work you hand an agent. GitHub's issue_labeled names the repository to watch, so the agent wakes when someone labels an issue there, one conversation per issue:
github = noclick.Connection.from_name("github", integration="github", create_if_missing=True)
triage = noclick.Agent.from_name(
"triage",
create_if_missing=True,
instructions="When an issue is labelled 'agent', reproduce the bug, then comment with what you found.",
tools=[github],
triggers=[noclick.OnEvent(github, "issue_labeled", repository="acme/api", conversation="issue-{issue.number}")],
)Wake it on a NoClick event
The events your webhooks receive can wake an agent too, with no server of yours in between: a job failing, a connection breaking, a channel halting.
triage = noclick.Agent.from_name(
"triage",
create_if_missing=True,
instructions="When a job fails, find out why from its error and tell the team in #ops what to do.",
tools=["slack.send_message_to_channel"],
triggers=[noclick.OnPlatformEvent("operation.failed", filters={"kind": "agent.task"},
message="A job failed.")],
)type is an event type, and * matches the rest of a name ("connection.*"). filters are dotted paths into the event's data and the values they must have: the one above hears only failed jobs. The turn reads message, then the event: its type, id, user and data. Each event is a conversation of its own unless conversation keys it, a template over the event ("connection-{data.id}").
An agent hears its user's events, and one of your account's agents hears every event, your users' included, as your account's webhooks do. It never hears what its own turns caused, so an agent listening for agent.turn.completed isn't woken by its own replies, and agents waking each other this way stop after five turns in a row. A NoClick event needs no URL and no connection, so its trigger is active from the start; it hears events from the moment it's added.
Run on a schedule
import noclick
digest = noclick.Agent.from_name(
"digest",
create_if_missing=True,
instructions="You summarize yesterday's support tickets for the team.",
tools=["search", "slack.send_message_to_channel"],
triggers=[noclick.Cron("0 8 * * 1-5", tz="Europe/London", message="Post the overnight summary in #support")],
)A Cron is a five-field cron expression (minute, hour, day of month, month, day of week) read in tz, an IANA timezone that defaults to UTC, so the run above happens at 8:00 London time on weekdays, daylight saving included. Each field takes numbers, *, ranges (1-5), steps (*/15), lists (1,15) and month or day names (jan, mon-fri); L, W, # and seconds aren't taken. When both day fields are restricted, a run must match both: 0 9 1-7 * 1 is 9:00 on the first Monday of each month, where plain cron would also run on the 1st to the 7th. message is the turn's text; without one, the turn just says the schedule fired. next_run on the trigger says when it fires next.
tz="user" reads the expression in the timezone of the user a user's agent belongs to, and follows them: when user.update(timezone=...) moves them, the next run is in their new zone, so a morning briefing follows its user when they travel (UTC while they have none; an account agent refuses it, having no user). Every turn is told the date and time too, so a message like "Summarize last month's invoices" means the month before the run (Tell it the time).
Each run is a conversation of its own, so a daily automation doesn't drag a growing history into every turn. What it should carry from one run to the next belongs in its memory. A schedule fires at most once a minute, its turn is your own words rather than something from outside, and it runs on NoClick's scheduler, the same one that runs schedules built on the canvas.
Wake it from a queue
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"})OnQueue makes each message on a queue a turn: the agent reads message, then the message's JSON body. The queue is a Queue or its name, from the agent's own scope. With batch_size (up to 25), one turn takes up to that many waiting messages at once, as a JSON list. concurrency (1 by default, up to 50) runs up to that many turns at once, for a queue that fills faster than one turn at a time drains it:
enrich = noclick.Agent.from_name("enrich")
enrich.add_trigger(noclick.OnQueue("signups", concurrency=10, message="Research this company and save what you find."))The queue's delivery rules apply. A turn starts as soon as a message is put (a delayed one, or one handed back, within about half a minute of becoming visible). A turn that finishes acknowledges its messages; a turn that fails hands them back for another try after a pause that grows with each attempt (30 seconds per attempt so far), and a message that fails the queue's max_attempts times becomes a dead letter, with the turn's error as its last_error. Each turn is a conversation of its own unless conversation keys it (below). While the agent is paused, messages wait on the queue rather than being skipped, and the agent works through them after resume. The trigger reads the queue by its name in the agent's own scope, and the queue can't be deleted while the trigger is there.
Running out of money isn't a failure. When the agent's budget, its user's spend cap or your account's funds run out, the trigger pauses: messages wait on the queue with their attempts unchanged, and the trigger's status is error, saying why. It looks again every half minute and carries on by itself once there's money again (the month turns, the cap goes up, funds arrive). An agent whose user's spending you've paused leaves its messages on the queue the same way, until you resume the user.
Keep related deliveries together
A webhook or app-event trigger takes conversation, a template over what arrived: each {...} is a dotted path into the webhook's JSON body or the event as the app sent it. Deliveries that render the same key continue one conversation, so an issue's comments, an order's updates or a customer's messages build on each other:
fulfilment = noclick.Agent.from_name("fulfilment")
fulfilment.add_trigger(noclick.OnWebhook(name="order-updates", conversation="order-{order.id}"))The key follows the same rules as one you choose yourself (1 to 128 letters, digits and . _ : @ + -, after rendering). A delivery that lacks a field the template names starts a conversation of its own, and a webhook's NoClick-Conversation header wins over the template (a header that isn't a valid key is refused with 400). One conversation takes one turn at a time: a delivery whose conversation is still answering an earlier one waits, pending, and starts within about half a minute of that turn ending, in the order they arrived. The turn's operation records the key it ran in as input["conversation"]. A queue trigger keys each turn by its one message (the template is over its JSON body), so it can't take conversation with a batch_size above 1; a message whose conversation is still answering waits a few seconds on the queue, its attempts unchanged.
Email and calls
Mail and calls wake an agent from where they're set up: an inbox created with agent=, a number routed with answer_with. triggers() lists them too, as kinds email and call, so you can see everything that wakes an agent in one place. Change them on the inbox or the number, not here.
An approval trigger is listed while your policy may hold one of the agent's tools for a person: a require_approval rule names one of them, with or without a condition. Its spec["tools"] names them as policy does (stripe.create_refund, human.ask). When the person answers, the agent takes a turn in the conversation the held call came from: with the call's result once it's approved and run, or with the decline and its note (When a call waits for approval). You change it in your policy: remove_trigger on it raises noclick.InvalidRequest. An agent's trigger_kinds leaves it out, since your policy, not the agent, decides it.
Keep triggers in your code
With create_if_missing=True, triggers= is part of the agent's definition like its instructions and tools. On first use, a trigger your code no longer lists is removed, and one it adds is created. A trigger that stays is left alone, so a webhook keeps its URL across deploys. Email and call triggers are never touched. triggers=[] removes every webhook and event trigger; leaving triggers out changes nothing. Agent.create(triggers=...) checks every definition before it makes anything, so a bad one leaves no agent behind.
Add and remove triggers
For agents you manage outside a definition, change triggers one at a time:
support = noclick.Agent.from_name("support")
mentions = support.add_trigger(noclick.OnEvent("slack", "app_mention", message="Someone asked you something."))
support.remove_trigger(mentions)
support.remove_trigger(noclick.OnWebhook(name="orders")) # or the definition you addedremove_trigger takes an AgentTrigger, its id, or a definition matching one the agent has (noclick.NotFound if none does): an OnWebhook by its name alone, since an agent's webhook names are unique, and any other definition as you added it. A trigger added this way is removed the next time a definition without it runs, so don't mix the two on one agent.
See what wakes an agent
support = noclick.Agent.from_name("support")
for trigger in support.triggers():
print(trigger.kind, trigger.status, trigger.url, trigger.last_fired_at, trigger.error)Each AgentTrigger has its kind (cron, webhook, event, email or call), its spec (what it was defined with), and a status: active, paused while its agent is, or error with the reason in error. A webhook has its url, a schedule its next_run. Every turn a trigger starts is an operation like any other, with the agent's agent_id and input["trigger"] naming the trigger. In the conversation, the message that turn read is a user message whose trigger says so, so a chat UI can tell it from one a person sent (Read conversations).
agent.trigger_kinds names the kinds without a read, which is how a listing tells automations apart:
automations = [agent for agent in noclick.Agent.list() if "cron" in agent.trigger_kinds]Pause an agent
digest = noclick.Agent.from_name("digest")
digest.pause()
print(digest.paused) # True
digest.resume()While an agent is paused, nothing wakes it: scheduled runs are skipped (not made up later), webhook calls are refused with 409 so their sender can retry, and app events and mail that arrive meanwhile don't wake it (the mail is kept, with agent_error saying so). A call to its number hears that it can't take calls right now; a call under way goes on. Messages your code sends still run, and so do wake-ups and the jobs it was given. A trigger added while it's paused waits paused too. After resume, a schedule picks up at its next run. Pausing and resuming are the agent owner's: a user can't pause your account's agent.
A trigger whose turns keep failing the same way, five in a row over at least 30 minutes, stops firing on its own: its status becomes error with the failure, its webhook answers 409, its schedule stops, and the schedule.paused event goes to your webhooks. Fix the cause, then resume() the agent, which starts every stopped trigger again. A turn that fails because your account can't pay, or because a spend cap was reached, never counts toward this.
Limits and billing
- An agent has up to 20 schedules, webhook and app-event triggers (email and call triggers don't count).
- Triggers are free. Each turn one starts is billed like any other turn, to the user the agent belongs to, and counts toward its budget and your spend caps. A delivery your account can't pay for comes back
failedwith the reason, and starts no turn. - A webhook body is at most 256 KB.
Errors
| Error | When |
|---|---|
noclick.InvalidRequest | An event the integration doesn't have (the error lists its events), a filter it doesn't take or a required one left out, a webhook name that isn't 1 to 64 letters, digits and . _ -, a conversation template that doesn't parse, a cron expression without five fields NoClick reads, a tz that isn't an IANA timezone, tz="user" on an account agent, or a batch_size above 25. |
ValueError | Before anything is sent: an OnQueue with conversation and a batch_size above 1, a batch_size that isn't a whole number of 1 or more, or a concurrency that isn't a whole number from 1 to 50. |
noclick.NotFound | OnEvent names a connection, or OnQueue a queue, the agent's own scope doesn't have, or remove_trigger matches nothing. |
noclick.Conflict | The agent already has a webhook trigger by that name. |
noclick.QuotaExceeded | The agent already has 20 triggers. |
noclick.PermissionDenied | Inside as_user, reading, changing, pausing or resuming your account's agent's triggers. |
noclick.Expired | Pausing or resuming an agent whose ttl_s ran out. |
Every method is in the reference: Agent.triggers, Agent.add_trigger, Agent.remove_trigger, Connection.events and the IntegrationEvent records it returns, Agent.pause, Agent.resume, and the definitions OnWebhook, OnEvent, OnPlatformEvent, Cron, OnQueue and AgentTrigger.