Skip to content

Hooks

A hook is a set of follow-up actions that Workprentice itself performs when an assignment’s run completes: post a private note on the support ticket the run was about, assign that ticket to someone, or start another assignment. Use one when a run’s result has to land in another system exactly once, in a fixed shape, whatever the agent happened to write along the way.

The difference from letting the agent make the write is who holds the pen. The agent researches and submits a structured result; the platform checks that result while the agent can still fix it, then makes the external write from it after the run ends. Because the write happens once, outside the agent’s turn, a retried webhook or a rerun cannot post the same note twice.

Hooks suit ticket-shaped intake: an assignment triggered by a webhook for each new ticket, whose output goes back onto that ticket.

An assignment’s Hooks tab shows the hook attached to it. The tab is read-only; hooks are written through the MCP endpoint, not in the app. With no hook attached it reads “This assignment has no hook definition, so nothing runs automatically when it finishes.” Otherwise it shows the hook’s name, an Enabled or Disabled badge, the event it runs on (assignment.completed), and four sections:

  • Capabilities: every action the hook may take, with the integration each one uses. An action not listed here cannot run.
  • Validation hooks: the contract a submitted work product must satisfy, with its content type and schema version.
  • Effects: each external action, its kind, the When: condition that decides whether it runs, and where its result is recorded.
  • Producers: the named inputs the effects are built from, such as the loaded work product or a rendered note.

Assignments with a hook attached show a Hooks badge in the assignment list.

A hook has three parts.

The contract is a JSON Schema for the work product the agent must submit. The check happens at submission: a result that does not match is refused with the reason, while the agent is still running and can correct it. The assignment’s instructions should tell the agent what to submit.

The rule runs when the run completes. Each effect can carry a when condition over the submitted result, such as summary.is_spam == false. Conditions are deliberately small: one field compared with == or != against a literal. They are checked against the contract’s schema when you save the hook, so a misspelled field is refused then rather than when a run completes.

The effects are the external actions. Each one takes its inputs from producers: the submitted result, a Markdown note rendered from a template, or a share link to the session.

Effect What it does
intercom.create_ticket_note Posts a note on an Intercom ticket.
intercom.assign_ticket Assigns an Intercom ticket.
zendesk.create_private_note Posts a private note on a Zendesk ticket; skipped if the ticket is closed.
zendesk.assign_ticket Sets a Zendesk ticket’s assignee.
workprentice.trigger_assignment Starts another assignment in the same workspace, so one run can follow another.

The Intercom and Zendesk effects use the workspace’s connection to that integration.

Each effect writes at most once per ticket. Workprentice takes the ticket id from the triggering webhook’s payload, never from the agent’s output, and records the write before it happens. A redelivered webhook or a rerun of the same ticket skips an effect that already succeeded. A run with no ticket id in its payload, such as a scheduled run, has nothing to key the write on, so the effect refuses to run.

When an effect fails cleanly (the ticket is closed, the credential was refused), it is recorded as failed and the next delivery for that ticket tries again. When the outcome is unknown, such as a timeout where the note may have posted, the effect is left alone rather than retried. Workprentice prefers a missed write to a double one.

Chained assignments stop after two hops past the original trigger, and a chain can never start an assignment already in it.

A run whose hook had anything worth reporting gets one message in its session: a failure, a write that was held back, or a hook that could not find its integration. Routine skips, such as an effect whose condition was false, stay silent. A run that ends in anything other than completed does not run its hook at all.

A webhook call with ?run_type=dry_run runs the whole assignment but holds back every external write. The note is still rendered and saved as a work product in the session’s side panel, so you can read exactly what would have been posted, and the session says: “This assignment ran in dry-run mode: its follow-up actions (such as a posted note or a ticket assignment) were computed but not sent.” The session header carries a dry-run marker, and the run is badged dry run on the assignment’s History tab. A chained assignment started from a dry run is a dry run too.

A dry run gates the hook only. The agent’s own tool calls during the run still happen, so test against records it is safe to change.

Hooks are written over the workspace MCP endpoint with an API key belonging to a workspace Admin. The hook tools are in the assignments toolset, which the default connection does not load, so connect with ?toolsets=assignments:

  • upsert_hook_definition creates or replaces a hook by its key. The whole definition is validated when you save it: an unknown field, an effect whose action is not a declared capability, or a when condition that names a field missing from the contract is refused with the reason.
  • list_hook_definitions and get_hook_definition read them back.
  • update_assignment attaches a hook by setting the assignment’s hook_definition_key to the hook’s key.

A definition has capabilities, validation_hooks (the contract), and completion_hooks (the rule, with its producers and effects). This one posts a note on each non-spam Intercom ticket:

key: ticket_triage
capabilities:
- action: intercom.create_ticket_note
integration: intercom
validation_hooks:
- event: work_product.submitting
id: summary_data
content_type: application/json
json_schema:
type: object
required: [ticket_id, is_spam, headline]
properties:
ticket_id: { type: string }
is_spam: { type: boolean }
headline: { type: string, maxLength: 80 }
completion_hooks:
- event: assignment.completed
producers:
load_summary:
type: work_product_json
id: summary_data
output_path: summary
render_note:
type: markdown_template
output_path: note
template: "**Triage:** {{ summary.headline }}"
effects:
- effect: intercom.create_ticket_note
when: "summary.is_spam == false"
inputs:
summary: { producer: load_summary }
note: { producer: render_note }
params: { ticket_id: $summary.ticket_id, body: $note, admin_id: 1234567 }

The contract’s id names a slot. The agent submits its result with slot: summary_data, and a condition over summary reads that slot. A params value starting with $ refers to an input by name. Send the definition to upsert_hook_definition as JSON; YAML is shown here for readability.

For the complete site index, see llms.txt.