Skip to content

Automations

An automation tells someone when something happens in a project. People make, change and test them on the Automations page, in the sidebar (Settings, Project, Automations leads there too), or at /api/v1/automations. An editor of a brand (the project’s Editor role, or a write grant on the brand) manages the automations for that brand’s releases; the project’s admins manage every one, and alone make those for any brand and every other trigger, since those reach private assets, brands and the audit log. Each person sees the ones they may manage.

  • A brand is released (brand.released): any brand of the project, or one named by brand. Each release fires once.
  • An asset’s license is about to end (asset.expiring): days days before an approved asset’s last day of use (its rights’ expires; 0 is the day itself). Each asset fires once per last day: a license renewed to a new date fires again as that one nears. Only the current version of a stack counts, and the check runs every five minutes.
  • Governance changes (governance): an entry of the project’s audit log, or a decision about an asset, named in actions. Name an action exactly (grant.set, key.created) or a family with .* (share.*); leave actions empty for every change, which makes the webhook a feed for your security tools. Asset decisions are asset.approved, asset.rejected, asset.archived, asset.unarchived, asset.made_current, asset.deleted, asset.restored, asset.accepted_tags, asset.dismissed_tags, asset.accepted_fields and asset.dismissed_fields. Entries outside a project (sign-ins, SSO, email domains, the organization itself) and a private asset’s decisions never fire: a project’s admins may not be allowed to see them.
  • The use check keeps refusing an asset (check.refused): one asset refused times times (1 unless said) within the last days days (7 unless said). Fires at most once per asset in each window of days. It counts what Insights records, so it stays quiet where usage insights are turned off.
  • Something new to fix (fix.opened): what Fixes lists and isn’t done yet, of kind: stale (a place still loads a replaced version) or gap (a search found nothing); leave kind out for both. Each fires once, and only what was first seen after the automation was made, so making one doesn’t send the backlog.
  • A proposal waits for review (review.waiting): an asset in review for days days or more, once each. Private assets don’t fire.

The sweep looks for refusals, Fixes and waiting proposals every five minutes; governance changes fire as they happen.

  • Email (email): to the addresses in emails, through the organization’s email settings, counted in its emails for the day.

  • Webhook (webhook): a POST to an https url, in format:

    • json (the default): the run’s JSON, below.
    • slack: a message for a Slack incoming webhook (https://hooks.slack.com/services/...): one line saying what happened, with a link.
    • teams: an Adaptive Card for a Teams workflow started by “When a Teams webhook request is received”, the same line and link.

    The URL is kept sealed and never shown again; the automation shows its host. Addresses on private networks are refused, and redirects are not followed.

Terminal window
curl -X POST https://artbucket.example.com/api/v1/automations \
-H "Authorization: Bearer $ARTBUCKET_KEY" -H "Content-Type: application/json" \
-d '{"name":"Rebuild the docs","trigger":"brand.released","brand":"default","action":"webhook","url":"https://ci.example.com/hooks/brand"}'

Every share link and new API key, to a Slack channel:

Terminal window
curl -X POST https://artbucket.example.com/api/v1/automations \
-H "Authorization: Bearer $ARTBUCKET_KEY" -H "Content-Type: application/json" \
-d '{"name":"Security","trigger":"governance","actions":["share.*","key.created"],"action":"webhook","format":"slack","url":"https://hooks.slack.com/services/T000/B000/XXXX"}'

The answer holds secret, once. Keep it: every POST is signed with it.

The Automations page starts one from a recipe: everything filled in but who it tells. Each is an ordinary automation, as the API takes it:

Recipe Body, before emails or url
New share links to Slack {"trigger":"governance","actions":["share.created","share.project","asset.published"],"action":"webhook","format":"slack"}
Access changes to the admins {"trigger":"governance","actions":["grant.*","group.members","key.created","key.revoked"],"action":"email"}
Licenses ending, to legal {"trigger":"asset.expiring","days":30,"action":"email"}
An asset the check keeps refusing {"trigger":"check.refused","times":3,"days":7,"action":"email"}
New Fixes to Teams {"trigger":"fix.opened","action":"webhook","format":"teams"}
Nudge reviewers {"trigger":"review.waiting","days":3,"action":"email"}
Approvals and rejections to Slack {"trigger":"governance","actions":["asset.approved","asset.rejected"],"action":"webhook","format":"slack"}
Rebuild a site on each release {"trigger":"brand.released","action":"webhook"}
{
"id": "6a1d...",
"event": "brand.released",
"brand": { "id": "...", "slug": "default", "name": "Acme" },
"release": 12,
"note": "Bluer primary",
"url": "https://artbucket.example.com/brands/default",
"at": "2026-10-14T09:30:00.000Z",
"automation": { "id": "...", "name": "Rebuild the docs" },
"project": { "id": "...", "slug": "library", "name": "Library" }
}

asset.expiring sends asset (id, filename, title), expires (the last day of use), days (left until it) and the asset’s url instead of the brand and release. The others send, beside id, event, url, at, automation and project:

Event Sends
governance action, target (what it was done to), actor, agent (done with an API key), detail (as the audit log keeps it)
check.refused asset, refusals, days
fix.opened kind; for stale: asset, replacement, place (a host, or an agent’s key name), fetches; for gap: q, searches
review.waiting asset, days (how long it has waited)
id is the run’s: a retry sends the same body, so a receiver can tell it from a new event.

Headers: Artbucket-Event names the event, and Artbucket-Signature signs the body (in every format; Slack and Teams ignore it) as Stripe signs its webhooks: t={unix seconds},v1={hex HMAC-SHA256 of "{t}.{body}"} with the secret. Check it before trusting the body, and refuse an old t:

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(body, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && v1?.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Send a test, on each automation, sends it now with a sample event, on or off, so a webhook or an inbox can be checked without releasing anything:

  • brand.released: its brand’s latest release (the default brand’s, for an automation on any brand); release 1 for a brand never released.
  • asset.expiring: the approved asset whose license ends soonest, else the newest approved asset, as if it ended in days days.
  • governance: the latest entry of the project’s audit log it takes, else one made up from its first action.
  • check.refused: that same asset, refused times times.
  • fix.opened: the first open Fix of its kind, else a made-up search.
  • review.waiting: the oldest proposal, else the oldest asset.

What it sends is the real thing’s, with "test": true. A test is tried once, never again, and lands in the run log marked Test. The audit log keeps who sent it.

Terminal window
curl -X POST https://artbucket.example.com/api/v1/automations/{id}/test \
-H "Authorization: Bearer $ARTBUCKET_KEY"

It answers the run, delivered or failed with the error, and a 422 when there is nothing to make a sample from (no brand, or no approved asset).

Each thing that fires an automation is a run, delivered at once. A webhook answering 2xx, or every email sent, delivers it. Anything else is tried again 5 minutes later, then an hour after that: three attempts in all, then the run is failed. An automation turned off fires nothing, and its runs waiting for a retry wait with it.

The run log, under each automation on the Automations page and at GET /api/v1/automations/{id}/runs, keeps its latest 50 runs: what set each off, and every attempt with the webhook’s status or the error. Making, changing and deleting an automation is in the audit log.