Skip to content

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.

The graph (workflow.graph) has exactly two keys:

{
"nodes": [ /* the tiles */ ],
"edges": [ /* connections between tiles */ ]
}
{
"id": "c1",
"type": "condition.cvss",
"params": { "op": "gte", "value": 9 },
"position": { "x": 320, "y": 140 }
}
FieldTypeDescription
idstringUnique identifier within the graph (free-form: trigger, c1, a1…).
typestringTile type (see the catalog).
paramsobjectTile parameters (depend on its type). {} if none.
positionobject (optional){ x, y } coordinates on the canvas. Omit it — the editor lays out tiles automatically.
{ "id": "e1", "source": "trigger", "target": "c1", "sourceHandle": "out" }
FieldTypeDescription
sourcestringid of the starting tile.
targetstringid of the destination tile.
sourceHandlestringOutput used on the source tile: out (triggers and actions) or true / false (conditions). Default: out.
idstring (optional)Connection identifier.

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: true and false. A connection leaving a condition must use sourceHandle true or false.
  • 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.

The full registry is also served by the internal API (GET /projects/{id}/workflows/node-types) to build the editor palette.

A trigger reacts to an event. Its context (CVE, EOL or certificate) determines which downstream conditions make sense. None take parameters.

TypeEventContextFires when…
trigger.cve_new_matchcve.new_matchCVEa new CVE matches your monitored stack.
trigger.cve_updatedcve.updatedCVEa tracked CVE is updated (score, etc.).
trigger.cve_poc_publishedcve.poc_publishedCVEa public PoC appears for a tracked CVE.
trigger.cve_kev_addedcve.kev_addedCVEa tracked CVE enters CISA’s KEV catalog.
trigger.eol_approachingeol.approachingEOLa tracked product’s end-of-life approaches.
trigger.eol_support_endingeol.support_endingEOLthe end of active support approaches.
trigger.eol_new_releaseeol.new_releaseEOLa new release of a tracked product ships.
trigger.url_cert_expiringurl.cert_expiringCertificatea monitored URL’s HTTPS certificate is about to expire.

Each condition has two outputs (true / false). A condition from a different family than the trigger (e.g. a CVE condition after an EOL trigger) has no useful effect.

TypeContextParameters
condition.severityCVEopgte | eq | in; value (one severity) for gte/eq, values (list) for in.
condition.cvssCVEopgt | gte | lt | lte; value number 0 → 10.
condition.epssCVEopgt | gte | lt | lte; value number 0 → 1.
condition.has_pocCVE(none) — true if a public PoC exists.
condition.is_kevCVE(none) — true if the CVE is in the KEV catalog.
condition.product_nameCVEopcontains | starts_with | ends_with | equals; value text (≤ 100).
condition.vendor_nameCVEsame as product_name, on the vendor.
condition.match_typeCVEvaluevendor | product | keyword (how the CVE matched your stack).
condition.case_statusCVEopeq | ne; value ∈ a case status or NONE.
condition.has_tagCVEtag_id (UUID of an organization tag).
condition.eol_days_remainingEOLop (numeric); value days −3650 → 3650.
condition.eol_is_pastEOL(none) — true if the end-of-life date has passed.
condition.eol_product_nameEOLop (text); value text (≤ 100).
condition.cert_days_remainingCertificateop (numeric); value days −3650 → 3650.
condition.cert_is_expiredCertificate(none) — true if the certificate has expired.
condition.url_containsCertificateop (text); value text (≤ 100).

Severities: CRITICAL, HIGH, MEDIUM, LOW, NONE, UNKNOWN. Case statuses: PENDING, ANALYZING, FIXING, ACCEPTED, RESOLVED, NOT_APPLICABLE.

Actions run when the flow reaches them. They have an out output and can be chained.

TypeParametersEffect
action.notify_inapp(none)In-app notification.
action.send_emailrecipients (optional): list of member IDs; empty = all relevant members.Email.
action.fire_webhookswebhook_id (optional): a specific webhook, empty = all active webhooks; formatjson | slack | discord.Outbound webhook (HMAC).
action.create_casestatus ∈ a case status (required).Creates an alert case. Required for an alert to appear under “Alerts” and for the email to include the “View alert” button.
action.ai_enrichinstruction (optional, ≤ 500): guidance for the analysis (e.g. “focus on Debian remediation”).Generates an AI analysis (summary, risk, remediation) from only the public technical data; the result is made available to the following actions (email, webhook, notification). No personal data is sent to the model.
action.add_tagtag_id (UUID, required).Adds a tag to the CVE in the project (auto-classification).

Valid (a workflow must have exactly one trigger), but does nothing.

{
"nodes": [
{ "id": "trigger", "type": "trigger.cve_new_match", "params": {} }
],
"edges": []
}

“When a new CVE hits my stack with CVSS ≥ 9, notify Slack.”

{
"nodes": [
{ "id": "trigger", "type": "trigger.cve_new_match", "params": {} },
{ "id": "c1", "type": "condition.cvss", "params": { "op": "gte", "value": 9 } },
{ "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.

“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.

{
"nodes": [
{ "id": "trigger", "type": "trigger.cve_kev_added", "params": {} },
{ "id": "ai", "type": "action.ai_enrich", "params": { "instruction": "Prioritize remediation." } },
{ "id": "mail", "type": "action.send_email", "params": {} }
],
"edges": [
{ "id": "e1", "source": "trigger", "target": "ai", "sourceHandle": "out" },
{ "id": "e2", "source": "ai", "target": "mail", "sourceHandle": "out" }
]
}

ai_enrich is placed before the email: its analysis is included in the message.

{
"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" }
]
}
MessageCause
Le workflow doit contenir exactement un déclencheurZero or several trigger.* tiles.
Sortie '…' invalide pour …Inconsistent sourceHandle (e.g. out on a condition, or true on an action).
Le workflow contient une boucleThe graph is not acyclic.
Un déclencheur ne peut pas avoir d'entréeA connection points to the trigger.*.
Paramètre requis '…' manquant pour …A required parameter is missing (e.g. value on condition.cvss).
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).
Trop de tuiles (max 30) / Workflow trop profond (max 20)Graph too large.
  • 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).