Skip to content

Patterns — three worked examples

Three concrete integrations, end to end, showing how the pieces in this section fit together. These are sketches that show the shape — adapt them to your stack. Where a recipe touches a specific request/response field, match the names in api-recipes.md (they're taken from the handler structs).


Pattern 1: Slack approval pinger

Goal: every time an agent emits /request-approval, post a message in #trimus-approvals so a human can resolve it without opening Trimus.

Pieces: SSE stream (sse-streaming.md), JSON API, Slack webhook.

[ trimus-server ]  ──SSE──→  [ pinger ]  ──webhook──→  [ Slack ]
                                  └─ on click → [ Slack action ] → PATCH /api/approvals/{id}
import requests, sseclient   # any SSE client works

BASE  = "https://trimus.example.com"
TOKEN = "sk_..."             # board-user key for a "slack-bridge" service account
SLACK = "https://hooks.slack.com/services/..."

# The approval_created event's data is the bare approval ID.
stream = sseclient.SSEClient(
    f"{BASE}/api/live/events",
    headers={"Authorization": f"Bearer {TOKEN}"})

for event in stream:
    if event.event != "approval_created":
        continue
    approval_id = event.data

    a = requests.get(f"{BASE}/api/approvals/{approval_id}",
                     headers={"Authorization": f"Bearer {TOKEN}"}).json()

    requests.post(SLACK, json={
        "text": (f":rotating_light: New approval pending\n"
                 f"*{a['title']}*\n"
                 f"<{BASE}/approvals/{approval_id}|Resolve in Trimus>")})

What you need on the Trimus side

  • A board-user account for the bridge (use a board-user or user key, not an agent key — only humans can resolve approvals; the loop-back PATCH /api/approvals/{id} needs that).
  • Run the pinger as a long-lived process. Reconnect with backoff and resend Last-Event-ID so a brief drop replays from the hub buffer (see sse-streaming.md).

Possible upgrades

  • Slack action buttons that POST to a small handler which calls PATCH /api/approvals/{id} with {"status":"approved","resolution":"…"}.
  • Ack-before-post: if losing a ping during a long outage is unacceptable, drive this from a worker (building-a-worker.md) instead of SSE.
  • Filter by role: only ping for approvals from senior agents.

Pattern 2: GitHub PR → Trimus issue bridge

Goal: when a PR opens in acme/payments, create a Trimus issue for the engineering-manager agent to review and route.

Pieces: GitHub webhook → a tiny signing relay → Trimus routine trigger (webhooks-in.md) → the agent creates the issue.

[ GitHub ] ──webhook──→ [ relay (verifies GitHub HMAC, re-signs for Trimus) ]
                                          ↓ POST .../fire  (X-Staple-Signature)
                              [ routine fires the agent ]
                              [ agent emits /create-issue ]

Doing the issue creation agent-side keeps the routing logic editable without redeploys and the relay dumb.

The Trimus side

  1. Create a routine ("github-pr-opened") targeting a triage or engineering-manager agent.
  2. Add a webhook trigger with signing_mode:"hmac" (via the UI or POST /api/routines/{routineId}/triggers). Copy the public_id and the one-time secret.
  3. Write the routine's prompt template to act on the payload:
A new GitHub PR opened.

Repo:   {{.payload.repository.full_name}}
Title:  {{.payload.pull_request.title}}
URL:    {{.payload.pull_request.html_url}}
Author: {{.payload.pull_request.user.login}}

Body:
{{.payload.pull_request.body}}

Create a Trimus issue tracking this PR with /create-issue, then
/assign the appropriate engineer for this repo.

The relay (the important bit)

Trimus's webhook signature is hex(HMAC-SHA256(rawBody, secret)) in X-Staple-Signature — it is not the raw secret, and it is not GitHub's signature. GitHub signs with its own secret, so you need a small relay that verifies GitHub's HMAC and re-signs the body for Trimus:

import hashlib, hmac, os, requests
from flask import Flask, request, abort

app = Flask(__name__)
GH_SECRET     = os.environ["GITHUB_WEBHOOK_SECRET"]
TRIMUS_SECRET = os.environ["TRIMUS_TRIGGER_SECRET"]
FIRE_URL      = os.environ["TRIMUS_FIRE_URL"]   # .../routine-triggers/public/<public_id>/fire

@app.post("/github")
def github():
    body = request.get_data()                    # raw bytes — sign exactly these

    # 1. Verify GitHub's signature (its scheme: sha256=<hex>).
    gh_sig = request.headers.get("X-Hub-Signature-256", "")
    expect = "sha256=" + hmac.new(GH_SECRET.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(gh_sig, expect):
        abort(401)

    # 2. Re-sign for Trimus and forward the SAME bytes.
    trimus_sig = hmac.new(TRIMUS_SECRET.encode(), body, hashlib.sha256).hexdigest()
    r = requests.post(FIRE_URL, data=body,
                      headers={"Content-Type": "application/json",
                               "X-Staple-Signature": trimus_sig})
    return ("", r.status_code)

You can skip the relay only if you set the trigger to signing_mode: "none" and lock the fire URL to GitHub's IP ranges at your proxy — Trimus has no built-in IP allowlist, so network-layer scoping is how you'd do it.

Possible upgrades

  • Close the loop on merge: a second trigger for pull_request.closed whose routine sets the matching issue to done.
  • Two-way sync: mirror Trimus comments back to the PR via a comment-watcher SSE consumer.

Pattern 3: Daily cost report

Goal: every weekday at 09:00, post a short cost summary as a comment on a "Daily ops" issue.

Pieces: cron + JSON API. No SSE, no webhooks, no plugin.

[ cron ] ──09:00──→ [ report script ] ──API──→ [ trimus-server ]
#!/usr/bin/env bash
set -euo pipefail
BASE="https://trimus.example.com"; TOKEN="sk_..."
COMPANY="<uuid>"; DAILY_OPS_ISSUE="<uuid>"

# Cost rollup (the /costs response is aggregated per agent/model/day —
# total however your reporting needs; this just grabs the raw feed).
COSTS=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/companies/$COMPANY/costs")

# Open budget incidents.
INCIDENTS=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/companies/$COMPANY/budget-incidents?status=open" | jq 'length')

BODY=$(printf 'Daily ops summary:\n\n- Cost feed: see dashboard\n- Open budget incidents: %s\n' "$INCIDENTS")

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg b "$BODY" '{body_markdown: $b}')" \
  "$BASE/api/issues/$DAILY_OPS_ISSUE/comments"
0 9 * * 1-5  /usr/local/bin/daily-report.sh

Possible upgrades

  • Build the report inside an agent instead of shell: a routine fires daily at 09:00 with a "Daily Reporter" agent that reads the cost API itself and writes a richer report. Same outcome, more flexibility, and no token in a cron script.
  • Cross-company: run per company, or (with instance-admin rights) walk the company list. Note each company still needs its own scoped key for the company-scoped reads.
  • Send to Slack as well as / instead of a Trimus comment.

Patterns by integration shape

You want… Use this
React in real time to one class of event SSE stream (Pattern 1)
Trigger Trimus from an external event Routine webhook (Pattern 2)
Run a periodic task that reads/writes Trimus Cron + API (Pattern 3)
Give agents a callable tool Plugin (writing-a-plugin.md)
Run heartbeats on your own infrastructure Worker (building-a-worker.md)
Bridge Trimus bidirectionally to a chat platform SSE outbound + routine inbound (1 + 2)
Mirror Trimus state into a warehouse Cron + API (append-only)

Where to next