Zendesk Support
Ein Runbook für eine Zendesk-Support-Warteschlange mit einem covey-Agenten.
Das Plugin kommt im Plugin-Pack
als zendesk/; der covey-Standardbinary importiert es, es gibt also nichts zu bauen
oder zu installieren — Zielsystem aktivieren, Credential hinterlegen, Wake-Quelle
wählen.
Kurzversion: ein Credential im SecretStore (
zendesk_url+zendesk_token), vier Auth-Formen, die der Tokenwert selbst auswählt, und zwei Wake-Quellen (Heartbeat ganz ohne Zendesk-Einrichtung, oder ein signierter Webhook). Das Ticket ist die Arbeitseinheit. Der Agent liest über/tickets/<id>/audits.jsonstatt über/comments, sieht damit seine eigenen internen Notizen und kann den Thread nicht in der falschen Reihenfolge lesen.
Wer das andere Helpdesk in diesem Pack sucht: Zammad ist in
zammad.md dokumentiert (bisher nur englisch).
Beide erledigen dieselbe Arbeit und unterscheiden sich an drei Stellen — das
Credential, wie ein Gespräch gelesen werden muss, und was „eskalieren" heißt.
1. Der Datenfluss
Zendesk ──(Trigger-Webhook, signiert)──────► covey /api/webhooks/zendesk/<agent-slug>
│ │ Signatur prüfen → Wake-Regel → Backlog-Aufgabe
│ ▼
│ Agent (Sandbox)
│ │ Aktionen über den Action-Proxy
Zendesk ◄──(REST /api/v2, gebrokertes Token)──────┘ list_tickets, list_messages, reply, escalate
▲
└─(Heartbeat, keine Zendesk-Einrichtung)── die Vorprüfung fragt: wartet etwas auf uns?
Zwei Richtungen, zwei Credentials:
- Raus (covey → Zendesk): REST gegen genau einen Host, die Konten-URL, authentisiert mit einem gebrokerten Credential, das nie in der Sandbox landet.
- Rein (Zendesk → covey): ein Trigger-Webhook mit Signing-Key — oder gar kein Eingang, wenn der Agent die Warteschlange per Heartbeat arbeitet.
2. Einrichtung
2.1 In Zendesk: eine Identität mit minimalen Rechten
Legen Sie einen eigenen Nutzer an („covey-Agent"), statt Ihren eigenen zu nehmen:
- Agent-Rolle, keine Admin-Rechte. Ein Token mit
read:messageswürde seinem Inhaber erlauben, als jeder Endnutzer zu schreiben; genau das soll dieser eine nicht können. - Setzen Sie ihn in genau die Gruppe(n), die der Agent arbeiten soll. Auf den Plänen Team und Growth gibt es Gruppenbeschränkungen nicht — dazu Abschnitt 7.
- Wenn der Agent Tickets in eine Eskalationsgruppe schieben soll, braucht er einen zweiten Trigger, und diese Gruppe muss ein echtes Ziel sein.
2.2 In covey: das Credential
Zwei Secrets pro Agent im SecretStore:
| Secret | Wert |
|---|---|
zendesk_url |
https://acme.zendesk.com — die Konten-URL, ohne /api/v2 |
zendesk_token |
eine der vier Formen unten |
Welche Auth-Form gilt, steht im Tokenwert selbst; das Plugin erkennt sie am Wert:
| Form | Wert | Anmerkung |
|---|---|---|
| API-Token | <mail>/<token> |
Der einfache Fall. Das Plugin schickt Basic, braucht also die Adresse, der das Token gehört — ein nacktes Token wird abgelehnt statt geraten |
| OAuth-Client | client:<id>/<secret> |
Mintet ein Token bei Bedarf und erneuert es selbst. Für eine Installation, die laufen bleiben soll, die richtige Form |
| OAuth-Refresh-Token | refresh:<refresh-token>/<client-id>/<client-secret> |
Funktioniert, aber jedes Erneuern verbrennt den alten Wert, und zurückschreiben ins SecretStore kann das Plugin nicht — Abschnitt 6 |
| Access-Token | <token> |
Ein anderswo gemintetes Token, oder ein Test. Das Plugin benutzt es unverändert und erneuert es nie |
Ein Agent, der nur eine Gruppe betreuen soll, nennt sie in der URL:
zendesk_url = https://acme.zendesk.com queue="Support L1"
Der Name ist der, den list_groups meldet, und die Anführungszeichen zählen —
Gruppennamen haben Leerzeichen. Das ist eine Grenze, kein Default: Der Agent
sieht die Tickets dieser Gruppe und keine anderen, auch dann nicht, wenn ein Kunde
eine Ticketnummer nennt, und auch beim Schreiben nicht. Die Heartbeat-Vorprüfung
erbt die Grenze, der Agent wird für eine fremde Gruppe also nicht einmal geweckt.
Anders als COVEY_ZENDESK_INTAKE_GROUPS (Abschnitt 3) gehört das zum Agenten und
nicht zur Installation: Welche Gruppe meine ist, ist eine Eigenschaft des
Mitarbeitenden, nicht der Maschine, auf der er läuft.
2.3 Zugriff und Guard-Rails
Die ACCESS.md des Agenten nennt das System und seine Scopes
(system: zendesk scope: read,write,comment). Schreibaktionen, die nach außen an
einen Kunden gehen, sind geteilt gegengeprüft von denen, die im Ticket bleiben, und
Eskalation ist noch einmal ein eigenes Gate — die Tabelle in Abschnitt 5.
2.4 Wake — Heartbeat, Webhook, oder beide
Per Heartbeat, ganz ohne Zendesk-Einrichtung. In HEARTBEAT.md:
alle: 15m
nur-wenn: zendesk
titel: Look after the support queue
aufgabe: Check the open tickets (list_tickets) for ones waiting for an answer,
read the conversation (list_messages) and reply.
nur-wenn: zendesk stellt eine Frage — wartet in meinem Bereich ein Ticket auf
uns? Sie kostet einen Listenlesen plus ein Lesen pro Ticket, das warten könnte,
höchstens COVEY_ZENDESK_PROBE_TICKETS viele (Standard 10). Die Liste wird ohne
Statusfilter gelesen — der Endpunkt nimmt genau einen, und „wartet auf uns" sind
new, open, pending und hold zusammen; clientseitig zu filtern kostet einen Aufruf
statt vier und landet auf denselben Tickets. Ein Ticket, dessen letzter öffentlicher
Kommentar von unserer eigenen Identität stammt, zählt nicht: Ein Agent, der
geantwortet hat, wird durch seine eigene Antwort nicht an dasselbe Ticket zurückgeholt.
Eine Kundenantwort erzeugt einen neuen öffentlichen Kommentar, also wird er wieder
geweckt. Die Prüfung gibt außerdem einen Fingerprint davon zurück, was wartet — ein
Agent, der ein Ticket gelesen hat und beschließt, nichts zu schreiben, wird eine
Minute später nicht durch denselben Stand erneut gestartet.
Per Webhook, wenn ein Ticket sofort aufgenommen werden soll. Im Admin Center (Apps and extensions → Trigger and automation webhooks):
- Endpoint URL:
https://covey.example.com/api/webhooks/zendesk/<agent-slug>— der Slug des zuständigen Agenten; die Agenten-ID (die UUID in der URL der Agentenseite) geht ebenfalls und ist auf einer Installation mit mehreren Organisationen die richtige Wahl, weil ein Slug nur innerhalb einer eindeutig ist. - Request method POST, Content-Type
application/json. - Signing: einschalten, und als Signing-Key den Wert von
COVEY_ZENDESK_WEBHOOK_SECRET.
Dann abonnieren: entweder ein Event-Trigger auf Ticket create / Ticket update
(das Konto postet dann ein Ticket-Event, Subjekt zen:ticket:<id>), oder ein
Trigger/Automation, das das Ticket selbst postet. Beide Payload-Formen werden
verstanden, und welche ankommt, entscheidet das Abonnement, nicht eine
Einstellung hier.
Die Signatur ist das, was diese Zustellung vertrauenswürdig macht. Zendesk postet
x-zendesk-webhook-signature: t=<unix-sekunden>,v1=<hex>über ein HMAC-SHA256 auft + "." + body, und das Plugin rechnet es mit dem Secret nach, in Hex. Ein Zeitstempel, der mehr als fünf Minuten von der Uhr der Plattform abweicht, wird abgelehnt — eine abgefangene Zustellung lässt sich nicht wiederverwenden. Das Schiefzeitfenster ist bewusst dieselbe Konvention wie dasWEBHOOK_MAX_SKEWder Plattform, wer einem Zeitstempel nach so langer Zeit nicht mehr trauen will, hat also einen Knopf dafür, und er gehört zur Plattform.Ein leeres
COVEY_ZENDESK_WEBHOOK_SECRETschaltet die Prüfung aus — so wird eine Entwicklungsumgebung markiert, genau wie bei Zammad. Produktiv ist das Secret gesetzt, ein unsigned eintreffender Webhook wird dann abgelehnt statt ausgeführt. Eine lokale Instanz, die nicht signieren kann, muss stattdessen per Heartbeat gearbeitet werden.
2.5 Prozess-Umgebung
COVEY_PUBLIC_URL=https://covey.example.com # von Zendesk aus erreichbar, nicht localhost
COVEY_ZENDESK_WEBHOOK_SECRET=<lang-zufaellige-zeichenkette> # identisch mit dem Signing-Key
2.6 Testen
- In der Zielgruppe ein Ticket anlegen — als Kunde.
- Taucht eine Backlog-Aufgabe beim Agenten auf? → Recording lesen.
- Wenn der Agent geantwortet hat: Ist die Antwort für den Kunden sichtbar (Abschnitt 5)?
- Als Kunde nachfragen: Wacht der Agent wieder auf, und taucht die frühere interne Notiz in dem auf, was er liest?
escalate: Ist das Ticket in die Eskalationsgruppe gewandert und haben seine Tags überlebt?
Ein fertiger Agent, der das alles benutzt:
examples/zendesk-support-agent.de.bundle.json —
ein Support-Agent, der die Warteschlange per Heartbeat arbeitet. Er braucht
nichts weiter als die beiden Secrets aus 2.2 und das aktivierte Zielsystem.
3. Welche Tickets der Agent aufnimmt
Drei Filter, von der Quelle nach innen. Jeder beantwortet eine andere Frage.
| Stufe | Wo | Frage |
|---|---|---|
| Trigger | Zendesk | Welche Ereignisse überhaupt zugestellt werden — Gruppe, Priorität, Kanal, Tag |
COVEY_ZENDESK_INTAKE_GROUPS |
covey, pro Installation | Welche Gruppen diese Installation bearbeitet. Leer = jede Gruppe |
queue= in zendesk_url |
covey, pro Agent | Welche Gruppe dieser Agent hat — eine Grenze, auch für Reads per ID und für alles Schreiben |
Der sauberste Filter sitzt an der Quelle: Was ein Trigger nicht zustellt, erreicht covey gar nicht. Die Umgebungsvariable ist das Auffangnetz für einen zu weit gefassten Trigger — und sie existiert hier, anders als ihr Zammad-Pendant, weil die Heartbeat-Vorprüfung dieselbe Einschränkung ohne jeden Webhook mittragen muss.
Der Gruppenfilter gilt für Tickets mit Gruppe. Ein Ticket ohne eine ist nicht „in jeder Gruppe" — es gehört in keine, und ein Warteschlangen-Agent hat damit nichts zu tun. Beim Heartbeat gilt dasselbe: Tickets ohne Gruppe wecken einen Agenten, der nach Gruppe arbeitet, nicht. Wer solche Tickets mitbearbeiten will, muss sie in eine Gruppe leiten.
4. Was der Agent liest
Das Gespräch wird aus dem Audit-Trail neu aufgebaut
(/tickets/<id>/audits.json), nicht aus /comments. Drei Gründe, alles Dinge, die
sonst passieren:
- Interne Notizen sind echte Antworten. Im Audit-Trail sind sie gleichwertige Ereignisse, und ein Agent kann eine ältere Antwort nicht übersehen, weil er nur die letzten drei Kommentare gelesen hat.
- Audits kommen neueste zuerst. Ein Thread, neueste-first gelesen, bringt einen Agenten dazu, die falsche Frage zu beantworten. Das Plugin dreht die Ereignisse um, bevor der Agent sie sieht.
- Es ist der einzige Ort, an dem überhaupt zu sehen ist, wann etwas versteckt wurde und von wem. Redigierte Kommentare werden als redigiert gemeldet, sie fehlen nicht stillschweigend.
Kommentare, die über das comments[]-Array des Tickets selbst geschrieben wurden
(der übliche Weg), erscheinen nicht als Audit-Ereignisse. Das Plugin faltet sie aus
dem Ticketkörper in die Zeitlinie ein, wenn es die Kommentar-ID kennt, damit die
Reihenfolge stimmt.
Namen. Der Agent arbeitet in Namen, nicht in Nummern: Gruppen, Anfragende,
Bearbeitende und Beteiligte kommen aufgelöst zurück, aus einem gebündelten
/users/show_many.json pro Liste. Eine Person, die für dieses Credential nicht
sichtbar ist, wird per ID gemeldet statt als geratener Name. Wenn /users/me nicht
lesbar ist, scheitert nichts: Der Read liefert die Namen, die er bekommen hat, und
der Heartbeat-Test „ist das unsere eigene Antwort?" fällt auf die Rolle des
Autors zurück — der schwächere der beiden Tests, und das Plugin weiß das.
Anhänge. list_attachments gibt ID, Name, Typ, Größe und Autor — und keine
Download-URL, denn die sind signiert und kurzlebig. download_attachment holt die
Datei und gibt einen Pfad in der Sandbox zurück
(attachments/<anhang-id>-<name>, damit zwei gleichnamige Dateien in einem Ticket
sich nicht gegenseitig überschreiben), wo der Vision-Schritt der Runtime sie ansehen
kann. Eine Datei von einem fremden Host wird abgelehnt, und ein file://-Pfad in
einer Antwort ebenso: die eine Adresse, die der Agent nicht abrufen darf, ist eine
lokale Datei.
5. Was der Agent schreibt
| Aktion | Geht raus | Guard-Rail-Subjekt |
|---|---|---|
reply (internal:true, der Default) |
privater Kommentar | zendesk:reply_internal |
reply (internal:false) |
öffentlicher Kommentar — der Kunde sieht ihn | zendesk:reply_external |
update_ticket |
die benannten Felder: Status, Priorität, Tags, Bearbeiter, Gruppe, Custom Fields | zendesk:update_ticket |
set_status |
ein Feld, damit „Ticket weiterbewegen" nicht heißt, ein ganzes Ticket zu benennen | zendesk:set_status |
escalate |
interne Notiz mit dem Grund, die Eskalationsgruppe wenn gesetzt, das Tag covey-escalated |
zendesk:escalate |
create_ticket |
ein neues Ticket, sein Text als erster Kommentar | zendesk:create_ticket |
attach_file |
eine Datei aus der Sandbox und den Kommentar, der sie trägt | zendesk:attach_file |
merge_tickets |
das Duplikat, gefaltet in das überlebende Ticket | zendesk:merge_tickets |
Das Scope-Vokabular dieses Systems ist read, write, comment; die Guard-Rails
greifen auf das Subjekt oben zu, denn das ist, was ein Lauf protokolliert und
wonach die Control Plane später fragt. Schreiben, das die Seite verlässt, und
Schreiben, das im Ticket bleibt, sind deshalb zwei verschiedene Gates, und
Eskalation ist ein drittes.
Der Default für reply ist intern. Eine falsche Antwort ist dann eine Notiz,
nicht eine Behauptung gegenüber einem Kunden — ein Agent, der antworten will,
schreibt ausdrücklich "internal": false.
Eine Antwort ändert den Status nicht, solange Sie es nicht anordnen.
COVEY_ZENDESK_REPLY_STATUS=pending macht, dass eine Antwort nach außen das Ticket
auf diesen Status setzt; ohne Angabe bleibt der Status, wie er ist, und die Antwort
sagt das auch. Der Status eines laufenden Tickets gehört dem Workflow, und ein Agent,
der ihn still weiterbewegt, versteckt eine Rückfrage. Wenn das Setzen scheitert —
eine Automation hat schon bewegt, der Workflow verbietet den Sprung — meldet sich die
Antwort trotzdem als versendet und trägt status_warning mit dem, was das Konto
gesagt hat: Die Antwort ist raus, und so zu tun, als nicht, würde einen Agenten dazu
bringen, sie noch einmal zu schicken.
escalate schreibt eine interne Notiz mit dem Grund, schiebt das Ticket in
COVEY_ZENDESK_ESCALATION_GROUP wenn eine gesetzt ist (leer = es behält seine
Gruppe), und fügt das Tag covey-escalated hinzu. Die Priorität bleibt unangetastet:
Ein Mensch, der ein Ticket für eilig hält, soll derjenige bleiben, der das gesagt
hat. Das Tag wird zusammengführt statt als Liste geschrieben, weil ein Tag-Update die
Liste ersetzt — ein Ticket eskalieren, indem still die Tags gelöscht werden, die
eine andere Abteilung draufgesetzt hat, wäre eine schlechte Art von Hilfe.
Ein blockierter Agent. Das vorgesehene Paar, wie bei Zammad: Der Agent antwortet
mit einer Rückfrage und setzt den Status auf pending. Die Kundenantwort kommt als
neuer öffentlicher Kommentar, der Trigger feuert, covey korreliert über die
Ticket-ID und setzt die Session fort. Ein Trigger nur auf ticket created heißt,
dass der Agent nie wieder aufwacht — abonnieren Sie auch Updates.
6. Credentials, die ablaufen
Die OAuth-Formen minten ein Token mit Ablauf. Der Probe — die Identitätszeile, die die Credential-Oberfläche zeigt — meldet das Ablaufdatum, das das Konto beim Minten genannt hat. Für ein Token, das dieser Prozess nicht gemintet hat, gibt es nichts zu melden, und das Plugin sagt das, statt ein Datum zu raten.
Client Credentials (client:) ist die Form für alles, was bleiben soll: Das
Plugin mintet bei Bedarf, behält nichts, und es gibt nichts zu rotieren.
Refresh-Token (refresh:) verbrennt den alten Wert bei jedem Erneuern. Das
Plugin behält den neuen im Speicher, solange der Prozess lebt, und kann ihn aus einem
Aufruf heraus nicht ins SecretStore zurückschreiben. Rotiert wird über die
Plattform: Rotate macht genau ein Erneuern und gibt das neue Credential zurück,
und die Control Plane speichert das Ergebnis. Ein Prozessneustart mit verbranntem
Refresh-Token ist ein Credential, das nicht mehr funktioniert — deshalb verweist das
Setup-Doc in zendesk/plugin.go für langlebige Setups auf Client Credentials.
7. Umgebungsreferenz
| Variable | Default | Bedeutung |
|---|---|---|
COVEY_PUBLIC_URL |
http://localhost:8494 |
Die Basis-URL, an die Zendesk den Webhook zustellt |
COVEY_ZENDESK_WEBHOOK_SECRET |
(leer = Prüfung aus, nur Entwicklung) | Signing-Key, identisch mit dem Signing-Key des Webhooks |
COVEY_ZENDESK_INTAKE_GROUPS |
(leer = jede Gruppe) | Welche Gruppen diese Installation arbeitet, per Name |
COVEY_ZENDESK_ESCALATION_GROUP |
(leer = Gruppe behalten) | Wohin escalate ein Ticket schiebt, per Name |
COVEY_ZENDESK_REPLY_STATUS |
(leer = Status unangetastet) | Eine von new open pending hold solved closed canceled — der Status, den eine Antwort nach außen setzt |
COVEY_ZENDESK_ATTACHMENT_MAX_MB |
25 |
Pro Datei, 1…50 — Zendesks eigene Obergrenze |
COVEY_ZENDESK_PROBE_TICKETS |
10 |
Wie viele Tickets die Heartbeat-Vorprüfung liest |
Jede warteschlangenartige Einstellung wird per Name gesetzt, und die Namen sind
die, die list_groups meldet — diese Aktion einmal ausführen, statt die Zeichenketten
aus dem Admin-Interface abzutippen.
Egress. Die Konten-URL ist der einzige Host, den das Plugin je anspricht, und
OAuth-Token werden bei demselben Host gemintet (https://<subdomain>.zendesk.com),
niemals bei einem generischen Authorisierungshost. Pro Agent also ein
Allowlist-Eintrag:
COVEY_EGRESS_ALLOW="acme.zendesk.com"
zendesk_urlist https, und reines http wird nur auf einer Loopback-Adresse akzeptiert (127.0.0.1, localhost, ::1) — so laufen eine lokale Instanz und der skriptierte Live-Test. Alles, was kein Laptop ist, wird beim Credential abgelehnt, bevor ein Client-Paar im Klartext hinausgehen könnte.
8. Bekannte Grenzen
- Audit-Pagination liest alle Seiten.
/tickets/<id>/audits.jsonhat kein serverseitiges Fenster, ein Ticket mit hunderten Audits kostet also einen Read pro 100 Ereignisse. So lange Threads sind im Support selten, und das Plugin tut nicht so, als wäre es anders;limitbeschneidet, was beim Agenten ankommt, nicht was gelesen wird. update_timein Suchtreffern kommt im Index als Unix-Sekunden und sonst überall als RFC 3339. Zeiten werden nachsichtig gelesen und als der String behalten, der angekommen ist.- Team- und Growth-Pläne haben keine Gruppenbeschränkungen: Das Credential sieht
jedes Ticket.
queue=hält dann den Agenten ehrlich, ist aber nicht durchsetzbar — auf diesen Plänen keine Sicherheitsgrenze daraus machen. - Ein Ticket ohne Gruppe wird von einem warteschlangenartigen Wake weder geweckt noch gelistet. „Ohne Bearbeiter und ohne Gruppe" ist ein Zustand, den manche Konten für Spam benutzen; wenn Arbeit daran hängt, in eine Gruppe leiten.
- Das Wake-Bucket ist pro Aktion, nicht pro Ticket. Das Subjekt, das ein Lauf
protokolliert, ist
zendesk:<aktion>(replyaufgeteilt in internal und external), und genau das ist die Zeichenkette, nach derWritesWorkSignaturespäter gefragt wird. Zwei Agenten, die zwei verschiedene Tickets beantworten, teilen sich damit ein Bucket und können an einem Watermark hängenbleiben. Ein Bucket pro Ticket bräuchte eine Signaturfrage, die die ID mitführt, und das Subjekt, mit dem dieses Plugin antwortet, ist auf die Aktion hin geformt — dieselbe Wahl wie bei Zammad, aus demselben Grund. - Redigierte Kommentare werden als redigiert gemeldet. Ihr Text ist an der Quelle weg, nicht vom Plugin versteckt.