Building a workflow (JSON schema)
A TechWatchAlert workflow is an automation graph: a trigger reacts to an event (new CVE, approaching end-of-life, expiring certificate…), conditions filter, and actions notify or act. The graph is described by a JSON object you manipulate through the visual editor, the Vigie assistant, or workflow import/export.
Overall structure
Section titled “Overall structure”The graph (workflow.graph) has exactly two keys:
{ "nodes": [ /* the tiles */ ], "edges": [ /* connections between tiles */ ]}A tile (node)
Section titled “A tile (node)”{ "id": "c1", "type": "condition.priorite", "params": { "critere": "au_dessus_seuil" }, "position": { "x": 320, "y": 140 }}| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier within the graph (free-form: trigger, c1, a1…). |
type |
string |
Tile type (see the catalog). |
params |
object |
Tile parameters (depend on its type). {} if none. |
position |
object (optional) |
{ x, y } coordinates on the canvas. Omit it — the editor lays out tiles automatically. |
A connection (edge)
Section titled “A connection (edge)”{ "id": "e1", "source": "trigger", "target": "c1", "sourceHandle": "out" }| Field | Type | Description |
|---|---|---|
source |
string |
id of the starting tile. |
target |
string |
id of the destination tile. |
sourceHandle |
string |
Output used on the source tile: out (triggers and actions) or true / false (conditions). Default: out. |
id |
string (optional) |
Connection identifier. |
Validation rules
Section titled “Validation rules”A graph is rejected (with an explicit message) if it breaks any of these rules:
- Exactly one trigger per workflow (
trigger.*). - A trigger cannot have an input (no connection points to it).
- Conditions have two outputs:
trueandfalse. A connection leaving a condition must usesourceHandletrueorfalse. - Triggers and actions have a single output:
out. - The graph is acyclic (DAG) — no loops.
- 30 tiles maximum (
MAX_NODES), depth 20 maximum from the trigger. - Tile
ids are unique; every connection references existing tiles. - Tiles not connected to the trigger are ignored at runtime (tolerated on save so a draft isn’t blocked).
- 10 workflows maximum per project.
Tile catalog
Section titled “Tile catalog”The full registry is also served by the internal API (GET /projects/{id}/workflows/node-types) to build the editor palette.
Triggers (trigger.*)
Section titled “Triggers (trigger.*)”A trigger reacts to an event. Its context (CVE, EOL or certificate) determines which downstream conditions make sense. None take parameters, except the compliance ones (below).
| Type | Event | Context | Fires when… |
|---|---|---|---|
trigger.cve_new_match |
cve.new_match |
CVE | a new CVE matches your monitored stack. |
trigger.cve_updated |
cve.updated |
CVE | a tracked CVE is updated (score, etc.). |
trigger.cve_poc_published |
cve.poc_published |
CVE | a public PoC appears for a tracked CVE. |
trigger.cve_kev_added |
cve.kev_added |
CVE | a tracked CVE enters CISA’s KEV catalog. |
trigger.cve_exploitee |
cve.exploitee |
CVE | a tracked CVE becomes known to be exploited (CISA KEV, VulnCheck, CERT-FR, EUVD). |
trigger.cve_correctif_disponible |
cve.correctif_disponible |
CVE | a fixed version is published for a tracked CVE that had none (once per CVE). |
trigger.cve_rancongiciel |
cve.rancongiciel |
CVE | a tracked CVE is linked to ransomware by CISA or VulnCheck (once per CVE). |
trigger.eol_approaching |
eol.approaching |
EOL | a tracked product’s end-of-life approaches. |
trigger.eol_support_ending |
eol.support_ending |
EOL | the end of active support approaches. |
trigger.eol_new_release |
eol.new_release |
EOL | a new release of a tracked product ships. |
trigger.url_cert_expiring |
url.cert_expiring |
Certificate | a monitored URL’s HTTPS certificate is about to expire. |
Conditions (condition.*)
Section titled “Conditions (condition.*)”Each condition has two outputs (true / false). A tile from a different family than the trigger (e.g. a CVE condition after an EOL trigger) has no data to read: it always returns false. The editor no longer offers them in its palette, and flags those that became stray after a trigger change.
| Type | Context | Parameters |
|---|---|---|
condition.priorite |
CVE | critere ∈ exploitee (CVE exploited according to the policy: KEV first, EPSS, PoC) | kev_en_tete (in the KEV catalog, ranked first) | au_dessus_seuil (CVSS at the alert threshold) | sous_seuil_exception (below the threshold, alerted because exploited). Reads the project’s prioritization policy, refined by the threshold of the stack item that matched the CVE. |
condition.has_poc |
CVE | (none) — true if a public PoC exists. |
condition.is_kev |
CVE | (none) — true if the CVE is in the KEV catalog. |
condition.product_name |
CVE | op ∈ contains | starts_with | ends_with | equals; value text (≤ 100). |
condition.vendor_name |
CVE | same as product_name, on the vendor. |
condition.match_type |
CVE | value ∈ vendor | product (how the CVE matched your stack). |
condition.case_status |
CVE | op ∈ eq | ne; value ∈ a case status or NONE. |
condition.has_tag |
CVE | tag_id (UUID of an organization tag). |
condition.eol_days_remaining |
EOL | op (numeric); value days −3650 → 3650. |
condition.eol_is_past |
EOL | (none) — true if the end-of-life date has passed. |
condition.eol_product_name |
EOL | op (text); value text (≤ 100). |
condition.cert_days_remaining |
Certificate | op (numeric); value days −3650 → 3650. |
condition.cert_is_expired |
Certificate | (none) — true if the certificate has expired. |
condition.url_contains |
Certificate | op (text); value text (≤ 100). |
Deprecated threshold tiles. condition.severity (op ∈ gte | eq | in; value or values, severities CRITICAL, HIGH, MEDIUM, LOW, NONE, UNKNOWN), condition.cvss and condition.epss (numeric op; value 0 → 10 or 0 → 1) duplicated the project’s prioritization policy (project settings, Prioritization tab): the “from which score” is now set there. They are no longer in the palette, but a graph that contains them still validates and runs exactly as before; the editor marks them “Deprecated” and offers to replace them with condition.priorite.
Case statuses: PENDING, ANALYZING, FIXING, MITIGATED, ACCEPTED, RESOLVED, NOT_APPLICABLE. condition.case_status accepts them all. action.create_case does not accept MITIGATED, and skips ACCEPTED at run time (the run log says why): these statuses require a measure, a justification and an expiry date, set by a person.
Actions (action.*)
Section titled “Actions (action.*)”Actions run when the flow reaches them. They have an out output and can be chained.
| Type | Parameters | Effect |
|---|---|---|
action.notify_inapp |
(none) | In-app notification. |
action.send_email |
recipients (optional): restricts to the listed member IDs; empty = all relevant members. ai_enrich (boolean, optional) and ai_instruction (≤ 500): see below. |
Email. |
action.fire_webhooks |
webhook_id (optional): a specific webhook, empty = all active webhooks; format ∈ json | slack | discord | teams; ai_enrich / ai_instruction as above. |
Signed outbound webhook (see Webhook signature), sent after the event is saved, once per project event. |
action.create_case |
status ∈ a case status, except MITIGATED (required). |
Creates an alert case, only if the flow reaches the tile. Required for an alert to appear under “Alerts” and for the email to include the “View alert” button. |
action.add_tag |
tag_id (UUID, required). |
Adds a tag to the CVE in the project (auto-classification). |
AI enrichment
Section titled “AI enrichment”ai_enrich: true on an action.send_email or action.fire_webhooks tile attaches a short analysis to the message: risk level, why, and first remediation steps. ai_instruction (≤ 500 characters) steers the wording — “focus on Debian remediation”, “answer in English”.
The analysis is produced from only the public technical data of the event. Neither user identities nor the project name are part of the request, and every email address is redacted, including from your own guidance.
Teams format
Section titled “Teams format”format: "teams" produces an Adaptive Card in the attachments envelope a Teams webhook expects (Power Automate). Since Office 365 connectors were retired, the legacy MessageCard is no longer rendered — this is the format to use.
Webhook signature
Section titled “Webhook signature”Every call is signed with the webhook’s secret, shown only once when it is created (and on each regeneration):
X-TWA-Timestamp: the send time, in Unix seconds;X-TWA-Signature:sha256=+ hex HMAC-SHA256 ofX-TWA-Timestamp + "." + raw body.
cve.exploitee is sent once per CVE and per project. cvss_score and epss_score are null when the score is unpublished: that is not a zero.
{ "event": "cve.exploitee", "project_id": "…", "triggered_at": "2026-10-13T18:00:00+00:00", "cve_id": "CVE-2026-0001", "severity": "CRITICAL", "cvss_score": null, "epss_score": null, "has_poc": false, "is_kev": true, "rancongiciel": false, "description": "…", "matched_type": "product", "matched_value": "openssl"}expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()assert hmac.compare_digest(expected, signature) and abs(time.time() - int(ts)) < 300The comparison rejects a timestamp five minutes off. A copy resent inside that delay still verifies: the sender does not keep signatures already sent.
The call goes out after the event is saved, once per project even when the workflow runs for every member; a transient error (timeout, 429, 5xx) is retried with increasing delays. The run log shows queued, then sent or error.
Building a workflow, from A to Z
Section titled “Building a workflow, from A to Z”1. The minimum: a trigger alone
Section titled “1. The minimum: a trigger alone”Valid (a workflow must have exactly one trigger), but does nothing.
{ "nodes": [ { "id": "trigger", "type": "trigger.cve_new_match", "params": {} } ], "edges": []}2. CVE above the threshold → Slack
Section titled “2. CVE above the threshold → Slack”“When a new CVE in my stack is above the project’s alert threshold, notify Slack.” The threshold itself (CVSS 7, 9…) is set in the project’s prioritization policy.
{ "nodes": [ { "id": "trigger", "type": "trigger.cve_new_match", "params": {} }, { "id": "c1", "type": "condition.priorite", "params": { "critere": "au_dessus_seuil" } }, { "id": "a1", "type": "action.fire_webhooks", "params": { "format": "slack" } } ], "edges": [ { "id": "e1", "source": "trigger", "target": "c1", "sourceHandle": "out" }, { "id": "e2", "source": "c1", "target": "a1", "sourceHandle": "true" } ]}The c1 → a1 connection uses sourceHandle: "true": the action only fires when the condition is true.
3. true / false branch
Section titled “3. true / false branch”“KEV → alert case + email; otherwise → simple in-app notification.”
{ "nodes": [ { "id": "trigger", "type": "trigger.cve_new_match", "params": {} }, { "id": "kev", "type": "condition.is_kev", "params": {} }, { "id": "case", "type": "action.create_case", "params": { "status": "PENDING" } }, { "id": "mail", "type": "action.send_email", "params": {} }, { "id": "inapp", "type": "action.notify_inapp", "params": {} } ], "edges": [ { "id": "e1", "source": "trigger", "target": "kev", "sourceHandle": "out" }, { "id": "e2", "source": "kev", "target": "case", "sourceHandle": "true" }, { "id": "e3", "source": "case", "target": "mail", "sourceHandle": "out" }, { "id": "e4", "source": "kev", "target": "inapp", "sourceHandle": "false" } ]}Actions chain (case → mail via out). The false branch leads to a lightweight notification.
4. AI-enriched email
Section titled “4. AI-enriched email”{ "nodes": [ { "id": "trigger", "type": "trigger.cve_kev_added", "params": {} }, { "id": "mail", "type": "action.send_email", "params": { "ai_enrich": true, "ai_instruction": "Prioritize remediation." } } ], "edges": [ { "id": "e1", "source": "trigger", "target": "mail", "sourceHandle": "out" } ]}The analysis is a property of the message, not a step in the graph: nothing left to order.
5. Approaching end-of-life → email
Section titled “5. Approaching end-of-life → email”{ "nodes": [ { "id": "trigger", "type": "trigger.eol_approaching", "params": {} }, { "id": "c1", "type": "condition.eol_days_remaining", "params": { "op": "lte", "value": 90 } }, { "id": "a1", "type": "action.send_email", "params": {} } ], "edges": [ { "id": "e1", "source": "trigger", "target": "c1", "sourceHandle": "out" }, { "id": "e2", "source": "c1", "target": "a1", "sourceHandle": "true" } ]}NIS2 compliance (conformite)
Section titled “NIS2 compliance (conformite)”Once NIS2 is enabled on a project, its compliance notices (remediation due dates, end of life, inventory reviews, accepted risks, and at organization level: incidents, suppliers, governance) are sent by workflows created on activation. They are edited like any other; their own tiles:
| Type | Scope | Parameters |
|---|---|---|
trigger.conformite_projet |
project workflow | evenements (required, list) among conformite.echeance_proche, conformite.echeance_depassee, conformite.echeance_escaladee, conformite.fin_de_vie_depassee, conformite.fin_de_vie_escaladee, conformite.revue_due, conformite.risque_a_approuver, conformite.risque_decide, conformite.risque_a_revoir. |
trigger.conformite_org |
organization workflow | evenements (required, list) among conformite.incident_echeance, conformite.fournisseur_a_evaluer, conformite.rapport_pret, conformite.fonction_attribuee. |
condition.evenement_conformite |
both | evenements (required, list) — true if the notice is one of them. |
After a compliance trigger, action.notify_inapp and action.send_email take destinataires (list): prevus (the person the notice concerns — owner, requester, approver… — or else the administrators), rssi (security lead), direction (management), incident (incident contact), admins_projet, admins_org. Empty = prevus. recipients adds organization members to the email. action.fire_webhooks only exists in a project workflow. Nobody receives the same notice twice on the same channel, even when two workflows target them.
{ "nodes": [ { "id": "trigger", "type": "trigger.conformite_projet", "params": { "evenements": ["conformite.echeance_escaladee"] } }, { "id": "mail", "type": "action.send_email", "params": { "destinataires": ["rssi", "direction"] } } ], "edges": [{ "id": "e1", "source": "trigger", "target": "mail", "sourceHandle": "out" }]}Common validation errors
Section titled “Common validation errors”| Message | Cause |
|---|---|
Le workflow doit contenir exactement un déclencheur |
Zero or several trigger.* tiles. |
Sortie '…' invalide pour … |
Inconsistent sourceHandle (e.g. out on a condition, or true on an action). |
Le workflow contient une boucle |
The graph is not acyclic. |
Un déclencheur ne peut pas avoir d'entrée |
A connection points to the trigger.*. |
Paramètre requis '…' manquant pour … |
A required parameter is missing (e.g. critere on condition.priorite). |
Valeur '…' invalide pour … |
Value outside the allowed list (severity, status, operator…). |
Valeur hors bornes pour … |
Number out of range (CVSS 0–10, EPSS 0–1, days −3650–3650). |
La tuile … n'est pas disponible dans un workflow d'organisation |
Project-only tile (or the reverse), e.g. action.fire_webhooks in an organization workflow. |
Trop de tuiles (max 30) / Workflow trop profond (max 20) |
Graph too large. |
Going further
Section titled “Going further”- Easiest: describe your need to Vigie (the AI assistant) on the Workflows page — it generates the graph, which you approve.
- The visual editor produces and reads this same JSON; exporting a workflow yields a re-importable
{ name, description, graph }file. - See also: Workflows (API).