Release Channels
Ein Release Channel ist ein benannter Zeiger von einer Vorlage auf eine ihrer Versionen. Ihr Code rendert den Channel, und wohin der Zeiger zeigt, entscheidet, welche Version er bekommt, ohne Deployment auf Ihrer Seite.
Jede Vorlage hat den Channel published: Das Veröffentlichen einer Version verschiebt ihn, und ihn verwendet ein Rendering, das keine Version nennt. Benannte Channels wie staging oder beta stehen daneben. Ein typischer Aufbau:
| Channel | Zeigt auf | Gerendert von |
|---|---|---|
published | Version 7 | Produktion (version weggelassen) |
staging | Version 8 | Ihrer Staging-Umgebung ("version": "staging") |
Sieht Version 8 auf Staging richtig aus, veröffentlichen Sie sie (oder verschieben published dorthin), und die Produktion rendert sie innerhalb einer Minute. Stellt sie sich als falsch heraus, rollen Sie zurück, und Version 7 ist wieder live.
Einen Channel rendern
Übergeben Sie den Namen des Channels als version:
{
"template": "invoice",
"version": "staging",
"data": { "number": "RE-2026-001" }
}
version | Rendert | Schlüssel | Pläne |
|---|---|---|---|
weggelassen oder published | Die veröffentlichte Version (und ihren Canary, falls es einen gibt) | Live und Test | alle |
| ein Channel-Name | Die Version, auf die der Channel zeigt (und seinen Canary, falls es einen gibt) | Live und Test | ab Starter |
| eine Nummer | Genau diese Version | Live und Test | alle |
latest | Die neueste gespeicherte Version, veröffentlicht oder nicht | nur Test | alle |
latest ist die neueste Version, die jemand gespeichert hat, im Editor oder mit formfeed templates push; nicht gespeicherte Änderungen in einem offenen Editor stehen in keiner Version. Ein Live-Schlüssel, der latest sendet, bekommt 400 invalid_request. Anders als latest ist ein Channel eine bewusste Entscheidung, die jemand getroffen hat; deshalb dürfen auch Live-Schlüssel ihn rendern. Nehmen Sie für eine Staging-Umgebung lieber einen Channel als latest: Er ändert sich nur, wenn ihn jemand verschiebt, nicht bei jedem Speichern.
Das Rendering hält fest, woher die Version kam:
"template": { "id": "tpl_03b04e8c168dfc7aa5fdcdcf3f54489c", "slug": "invoice", "version": 8, "channel": "staging", "canary": false }
channel ist null, wenn die Anfrage eine Nummer oder latest nannte. Dasselbe template-Objekt steht in GET /renders/{id} und in den Webhook-Ereignissen.
Ein Channel, den die Vorlage nicht hat, antwortet mit 404 channel_not_found. Eine Vorlage ohne veröffentlichte Version antwortet für published weiterhin mit 404 template_not_found. Im Free-Plan antwortet jeder Channel außer published mit 403 forbidden (siehe Pläne).
Die API speichert 60 Sekunden lang zwischen, worauf ein Channel zeigt, genau wie beim Veröffentlichen. Nach einer Verschiebung verwenden Renderings die neue Version innerhalb einer Minute; bis dahin können einige noch die vorherige verwenden.
GET /templates/{id}/versions/staging liest die Version des Channels mit allen Dateien, genauso wie published oder eine Nummer. Derselbe Name funktioniert überall, wo eine Version gelesen oder geprüft wird: GET /templates/{id}/schema?version=staging liefert das Datenschema dieser Version, und POST /templates/{id}/validate mit "version": "staging" prüft Daten dagegen.
In Zapier, Make und n8n
- Zapier: Die Aktion Create Document zeigt ein Feld Version, sobald die Vorlage einen Channel neben
publishedhat, und listet jeden Channel mit der Version, die er rendert; eine Vorlage ohne benannte Channels zeigt gar kein Versionsfeld. - Make: Das Modul Create a document verhält sich genauso, und in das Select lässt sich auch eine Versionsnummer mappen.
- n8n: Die Option Version Name or ID des Nodes (unter den Optionen) listet immer Published und jeden benannten Channel mit seiner Version und nimmt auch eine Versionsnummer als Expression.
Channel-Namen
1 bis 32 Kleinbuchstaben, Ziffern und Bindestriche, beginnend mit einem Buchstaben: staging, beta, customer-a. latest und draft sind reserviert, und published gibt es immer; er lässt sich nicht löschen (DELETE …/channels/published antwortet mit 400 invalid_request). Jeder andere Name, etwa Staging oder 2026-q4, antwortet mit 422 channel_name_invalid.
Einen Channel verschieben
Das Verschieben eines Channels, den es noch nicht gibt, legt ihn an. published zu verschieben ist dasselbe, wie diese Version zu veröffentlichen: Die bisher veröffentlichte Version wird archiviert. published lässt sich in jedem Plan verschieben; jeder andere Channel braucht Starter oder höher. Jede Verschiebung merkt sich als previous_version, worauf der Channel vorher zeigte; dorthin kehrt das Zurückrollen zurück.
- In der App
- CLI
- API
- SDKs
Die Vorlagenliste zeigt die benannten Channels jeder Vorlage in der Spalte Kanäle. Um sie zu ändern, öffnen Sie die Vorlage im Editor und dort das Panel Versionen:
- Der Abschnitt Kanäle oben listet jeden Channel mit der Version, auf die er zeigt, seinem Canary (Canary v8 · 10 %) und pro Version den Renderings und fehlgeschlagenen Renderings der letzten 24 Stunden.
- Jede Version hat Kanal hierher verschieben: Wählen Sie einen Channel, um ihn auf diese Version zeigen zu lassen, oder Neuer Kanal …, um dort einen anzulegen. Jede Version zeigt außerdem die benannten Channels, die auf sie zeigen.
- Das Menü ⋮ neben einem Channel (Kanal-Aktionen) bietet Canary starten … (oder Canary ändern …), Canary v… übernehmen, Canary beenden, Zurück auf v… und bei benannten Channels Kanal löschen.
Inhaber, Admins und Editoren verschieben Channels; im Free-Plan weist das Panel darauf hin, dass Channels ab Starter verfügbar sind.
npx formfeed channels list invoice # Versionen, Canaries und Renderings der letzten 24 Stunden
npx formfeed channels set invoice staging 8 # staging anlegen oder auf Version 8 verschieben
npx formfeed templates push invoice --channel staging # die lokalen Dateien als neue Version pushen und staging dorthin verschieben
templates push --channel verschiebt den Channel auch, wenn sich die Dateien nicht geändert haben: Er zeigt dann auf die Version, die Ihr letzter Pull oder Push festgehalten hat. Der Channel wird dabei ohne Canary gesetzt; ein Canary, den er hatte, endet also.
curl -X PUT https://api-eu.formfeed.dev/v1/templates/invoice/channels/staging \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "Content-Type: application/json" \
-d '{ "version": 8 }'
| Feld im Body | Typ | Standard | Bedeutung |
|---|---|---|---|
version | Integer, Pflicht | Die Version, auf die der Channel zeigt (seine Hauptversion). | |
canary | Objekt oder null | null | { "version": 9, "percent": 10 }: eine zweite Version, die percent (1 bis 50) der Renderings bekommt. Weggelassen oder null entfernt den Canary. Siehe Canary-Releases. |
allow_breaking | Boolean | false | Trotzdem fortfahren, obwohl die Schema-Prüfung brechende Änderungen findet. |
{
"name": "staging",
"version": 8,
"canary": null,
"previous_version": 6,
"updated_at": "2026-09-13T09:12:00Z",
"schema_check": { "source": "stored", "breaking": [], "safe": [] }
}
201, wenn der Channel angelegt wurde, 200, wenn er verschoben wurde. Eine Versionsnummer, die die Vorlage nicht hat, antwortet mit 404 template_not_found. GET /templates/{id}/channels listet published und alle benannten Channels mit usage_24h (Renderings und fehlgeschlagene Renderings pro Version) und den limits des Plans ({ "channels": 2, "canary": false }; channels ist null, wenn unbegrenzt).
await client.templates.channels.set('invoice', 'staging', { version: 8 });
const { data, limits } = await client.templates.channels.list('invoice');
client.templates.channels.set("invoice", "staging", version=8)
channels = client.templates.channels.list("invoice")
Channels zu schreiben braucht den Scope template:write, sie aufzulisten template:read.
Canary-Releases
Ein Channel kann einen Anteil seiner Renderings an eine zweite Version schicken, den Canary, während der Rest weiter die Hauptversion verwendet. So probieren Sie eine neue Version mit echtem Traffic aus, bevor alle sie bekommen.
npx formfeed channels set invoice published 7 --canary 8 --percent 10
PUT /v1/templates/invoice/channels/published
{ "version": 7, "canary": { "version": 8, "percent": 10 } }
- Der Anteil ist eine ganze Zahl von 1 bis 50 Prozent, und der Canary muss eine andere Version als die Hauptversion sein; sonst antwortet die API mit
400 invalid_request. - Canary-Releases brauchen Pro oder höher (Pläne); auch
publishedkann einen Canary haben. template.canaryisttruebei Renderings, die der Canary-Anteil gewählt hat.- Einen Channel ohne
canaryzu setzen, entfernt seinen Canary. Um einen Canary zu beenden, setzen Sie den Channel erneut auf seine Hauptversion (Canary beenden in der App). - Vergleichen Sie die Fehlschläge des Canary mit denen der Hauptversion in
formfeed channels list, im Panel Versionen oder inusage_24h, und promoten oder entfernen Sie ihn dann. Automatisch wird nichts promotet oder zurückgerollt.
Wer den Canary bekommt
Jedes Rendering wird per Hash der Hauptversion oder dem Canary zugeordnet, sodass der Anteil im Mittel stimmt und dieselbe Eingabe immer auf derselben Seite landet. Was in den Hash eingeht:
| Die Anfrage sendet | Auf derselben Version landen |
|---|---|
canary_key | Alle Renderings mit diesem Wert, z. B. alle Dokumente eines Kunden |
keinen canary_key | Nur Wiederholungen derselben Anfrage (ihr Idempotency-Key); getrennte Renderings werden unabhängig verteilt, ohne Schlüssel zufällig |
einen Batch ohne canary_key | Alle Einträge dieses Batches; setzen Sie canary_key am Batch oder an einzelnen Einträgen, um stattdessen nach Kunde zu verteilen |
Senden Sie canary_key, wenn zusammengehörige Dokumente gleich aussehen sollen, etwa Auftragsbestätigung und Rechnung derselben Bestellung:
{ "template": "invoice", "data": { "…": "…" }, "canary_key": "customer-4711" }
canary_key ist ein String mit 1 bis 200 Zeichen. In einem Batch gilt ein canary_key auf oberster Ebene für jeden Eintrag, der keinen eigenen hat. Verwenden Sie eine neutrale ID statt einer E-Mail-Adresse; der Wert wird nur gehasht und nie gespeichert. Verwenden Sie dafür keinen wiederholten Idempotency-Key: Auf einen wiederholten Schlüssel antwortet die API 24 Stunden lang mit der gespeicherten Antwort des ersten Renderings, jedes spätere Dokument wäre also wieder das erste.
Promoten und zurückrollen
| Aktion | Was passiert |
|---|---|
| Promoten | Der Canary wird die Hauptversion des Channels, und der Canary wird entfernt; die alte Hauptversion wird previous_version, ein Zurückrollen macht das Promoten also rückgängig. Antwortet mit 400 invalid_request, wenn der Channel keinen Canary hat. In der App: Canary v… übernehmen. |
| Zurückrollen | Der Channel kehrt zu der Version zurück, auf die er vor seiner letzten Verschiebung zeigte (previous_version), und ein Canary wird entfernt. Ein zweites Zurückrollen kehrt dorthin zurück, wo das erste begann. Antwortet mit 400 invalid_request, wenn es keine frühere Version gibt. In der App: Zurück auf v…. |
| Löschen | Entfernt einen benannten Channel. Wird mit 409 channel_in_use abgelehnt, solange Renderings der letzten Stunde ihn verwendet haben, außer Sie erzwingen es. Renderings, die ihn danach noch nennen, schlagen mit 404 channel_not_found fehl. In der App: Kanal löschen, dann Trotzdem löschen, wenn er in Gebrauch ist. |
Promoten und Zurückrollen nehmen wie eine Verschiebung allow_breaking im Body, weil die Schema-Prüfung auch für sie läuft.
npx formfeed channels promote invoice published
npx formfeed channels rollback invoice published
npx formfeed channels delete invoice beta --force
await client.templates.channels.promote('invoice', 'published');
await client.templates.channels.rollback('invoice', 'published');
await client.templates.channels.delete('invoice', 'beta', { force: true });
client.templates.channels.promote("invoice", "published")
client.templates.channels.rollback("invoice", "published")
client.templates.channels.delete("invoice", "beta", force=True)
Über HTTP sind das POST …/channels/{name}/promote, POST …/channels/{name}/rollback und DELETE …/channels/{name}?force=true.
Pläne
| Free | Starter | Pro | Business | Enterprise | |
|---|---|---|---|---|---|
published | ja | ja | ja | ja | ja |
| Benannte Channels pro Vorlage | keine | 2 | 10 | 10 | unbegrenzt |
| Canary-Releases | nein | nein | ja | ja | ja |
- Im Free-Plan antwortet das Anlegen oder Rendern eines benannten Channels mit
403 forbiddenundfeature: "channels". Nach einem Downgrade auf Free antworten Renderings, die einen Channel nennen, ebenso; Renderings vonpublishedfunktionieren weiter. - Einen Channel über die Anzahl des Plans hinaus anzulegen, antwortet mit
403,feature: "channels"und demlimit. Nach einem Downgrade von Pro auf Starter funktionieren alle bestehenden Channels weiter und lassen sich weiter verschieben; nur neue werden abgelehnt. - Einen Canary ohne Canary-Releases zu setzen, antwortet mit
403undfeature: "canary". Ein Channel, der bereits einen Canary hat, rendert in einem solchen Plan nur seine Hauptversion; ein Downgrade bricht also keine Renderings.
Die Schema-Prüfung
Bevor eine Version live geht, vergleicht Formfeed ihr Datenschema mit dem Schema der Version, die Aufrufer gerade verwenden. Die Prüfung läuft immer dann, wenn ein Channel auf eine andere Version zeigen soll:
- beim Veröffentlichen (
POST /templates/{id}/versions/{number}/publishundPOST /templates/{id}/versionsmit"publish": true); - beim Verschieben eines Channels, auch
published, für die Hauptversion und den Canary; - beim Promoten und Zurückrollen.
Die Antwort enthält das Ergebnis als schema_check:
"schema_check": {
"source": "stored",
"breaking": [
{ "path": "customer.email", "pointer": "/properties/customer/properties/email", "kind": "field-became-required", "breaking": true, "message": "\"customer.email\" is now required" }
],
"safe": [
{ "path": "notes", "pointer": "/properties/notes", "kind": "field-added-optional", "breaking": false, "message": "New optional field \"notes\"" }
]
}
Jede Änderung nennt das Feld als Punkt-Pfad path (items[] für die Einträge eines Arrays, leer für die ganzen Daten), einen JSON-pointer in das Schema, ob sie breaking ist, eine lesbare message (auf Englisch) und ihre Art kind: field-added-required, field-added-optional, field-removed, field-became-required, field-became-optional, type-narrowed, type-widened, enum-values-removed, enum-values-added, enum-added, enum-removed, const-changed, constraint-tightened, constraint-relaxed, additional-properties-closed, additional-properties-opened oder keyword-changed. Eine abgelehnte Anfrage trägt dieselben Listen breaking und safe in ihrem Problem.
source | Wann | Wirkung |
|---|---|---|
stored | Beide Versionen speichern ein Datenschema | Brechende Änderungen lehnen die Anfrage mit 409 schema_breaking_change ab, außer sie sendet allow_breaking: true |
inferred | Mindestens eine Version hat kein gespeichertes Schema; verglichen wurden aus den Beispieldaten abgeleitete Schemas | Nur eine Warnung, nie eine Ablehnung: Ein abgeleitetes Schema ist kein Versprechen |
none | Nichts zu vergleichen: ein neuer Channel, das erste Veröffentlichen, dieselbe Version oder gar keine Daten | Nichts |
Was als brechend gilt
Die Prüfung betrachtet das Schema aus Sicht des Aufrufers, der Daten sendet, die die Vorlage liest. Eine Änderung ist brechend, wenn Daten, die zum alten Schema passten, vom neuen abgelehnt werden können.
| Brechend | Unbedenklich |
|---|---|
| Ein neues Pflichtfeld | Ein neues optionales Feld |
| Ein optionales Feld wird Pflicht | Ein Pflichtfeld wird optional |
Ein Typ wird enger: ["string", "null"] zu "string", number zu integer, {} zu einem Typ | Ein Typ wird weiter |
| Ein Enum-Wert entfällt, oder ein Feld ohne Enum bekommt eines | Ein Enum-Wert kommt hinzu, oder das Enum entfällt |
const kommt hinzu oder ändert sich | const entfällt |
minimum, maximum, minLength, maxLength, minItems, maxItems und ähnliche werden strenger; ein pattern oder format kommt hinzu oder ändert sich | Diese Einschränkungen werden gelockert oder entfallen |
additionalProperties wird false | additionalProperties wird wieder geöffnet |
| Ein Feld entfällt aus einem Objekt, das keine unbekannten Felder annimmt | Ein Feld entfällt aus einem offenen Objekt (Aufrufer dürfen es weiter senden) |
| Jedes andere Schlüsselwort, das sich unterscheidet, weil die Prüfung es nicht beurteilen kann | Beschreibungen, Titel, Beispiele und Standardwerte |
Trotzdem fortfahren
Manchmal ist eine brechende Änderung gewollt, zum Beispiel weil jeder Aufrufer das neue Feld bereits sendet. Dann sagen Sie es:
- App: Der Veröffentlichen-Dialog listet die brechenden Änderungen; wählen Sie Trotzdem veröffentlichen (Trotzdem verschieben, wenn Sie einen Channel verschieben).
- CLI: Hängen Sie
--allow-breakingantemplates push --publish,templates push --channel,channels set,channels promoteoderchannels rollback. Ohne das Flag gibt die CLI die Änderungen aus und endet mit Exit-Code1. - API: Senden Sie
"allow_breaking": trueim Body der Anfrage zum Veröffentlichen, zum Channel oder zur Version. - SDKs:
versions.publish(id, 8, { allowBreaking: true })undchannels.set(id, name, move, { allowBreaking: true })in TypeScript,publish(id, 8, allow_breaking=True)undchannels.set(…, allow_breaking=True)in Python.
Erzeugen Sie danach Ihre Typen neu, damit auch die Compiler der Aufrufer die Änderung sehen.
Ein typischer Ablauf
- Nach Staging pushen.
npx formfeed templates push invoice --channel staginglegt aus Ihren Dateien eine Version an und läsststagingdarauf zeigen. - Testen. Ihre Staging-Umgebung rendert mit
"version": "staging"und einem Test-Schlüssel, das kostet also nichts. - Live gehen. Veröffentlichen Sie die Version (
formfeed channels set invoice published 8oder Veröffentlichen im Editor). Die Schema-Prüfung läuft; passen Sie die Daten an oder erlauben Sie die Änderung. - Oder schrittweise ausrollen, ab Pro:
formfeed channels set invoice published 7 --canary 8 --percent 10,formfeed channels list invoicebeobachten, dannformfeed channels promote invoice published. - Rückgängig machen, falls nötig:
formfeed channels rollback invoice published.
In der CI kann der Schritt, der auf dem Standard-Branch templates push --publish ausgeführt hat, stattdessen bei jedem Merge nach staging pushen, und das Veröffentlichen wird ein eigener, manueller Schritt.