Workflows

Last updated on May 21, 2026

A workflow ties a trigger event to a chain of actions, with optional control flow in between, and you author it from a visual editor. Workflows move through a status lifecycle of DraftActivePausedArchived, and only Active workflows respond to events.

Anatomy

Every workflow you build is composed of three parts that flow into one another in order.

Trigger: an event fires.
Control flow (optional): a filter, if/else, or switch shapes the path.
Actions: nodes execute in sequence.

Triggers

A workflow has exactly one trigger, and each trigger exposes event variables that downstream nodes can reference.

TriggerFires when
run_createdA test run is created
unit_createdA unit is created
batch_createdA batch is created
part_createdA part is created
revision_createdA part revision is created
scheduleTime-based (daily/weekly/monthly), with timezone and day selection
manualOn demand from the editor

Rate limits: 100 automatic triggers per minute. 10 manual triggers per minute per organization.

Control flow

Control-flow nodes sit between the trigger and the actions, and they decide which downstream paths execute.

NodeBehavior
FilterSingle condition gate. Stops downstream when false.
If/ElseTwo-path branch on a true/false condition.
SwitchMulti-path branch with up to 20 cases plus a default path.

Conditions support equals, contains, starts with, greater than, regex match, is null, and more, and you can combine them with AND/OR.

Actions

Actions are the nodes that do work, and they are grouped by the system they talk to.

Messaging

Messaging actions push a message into a channel or inbox you already use.

Send email

Up to 50 recipients with variable interpolation.

Send Slack message

Post via incoming webhook URL.

Send Discord message

Post via webhook URL.

HTTP

The HTTP action calls any external API, so you can wire TofuPilot events into systems without a first-party integration.

HTTP request

Call external APIs with configurable headers, body, timeout, retries.

Odoo

Odoo actions read and write records in your Odoo instance.

Create record

Create a record on any Odoo model.

Update record

Update an existing record by id.

Post message

Post on the chatter of an Odoo record.

InvenTree

InvenTree actions sync test data and stock movements with your InvenTree instance.

Upload test result

Push a phase or run outcome onto a stock item's test report.

Update stock status

Change the status of a stock item.

Transfer stock

Move a stock item between locations.

Complete build output

Mark a build output as complete.

Print label

Trigger a label print.

Linear

Linear actions open and comment on tickets in your Linear organization.

Create issue

Optional assignee, labels, estimate.

Add comment

Comment on an existing issue.

Utilities

Utility actions cover pacing, math, and a little bit of operator-facing polish.

Delay

Pause execution for 1 second to 1 hour.

Math operation

Add, subtract, multiply, divide, modulo.

Celebrate

Trigger confetti for manual triggers.

HTTP request: 1-60 second timeout (default 10), 1 MB max body, up to 3 retries. Email: 50 recipients max.

Variables

Variables let downstream nodes read values produced upstream, and you reference them with {{nodeId.fieldKey}}.

  • Trigger variables: fields from the event.
  • Control flow variables: filter result and the branch taken in if/else or switch.
  • Action output variables: HTTP response status, created record id, and so on.

Missing variables render as [not set].

Editor

The editor is a node-and-edge canvas with shortcuts that mirror what you would expect from a graph editor.

  • Click + below any node or use the bottom toolbar to add nodes.
  • Click a node to configure it in the right panel.
  • Connect nodes by dragging between handles.
  • Add notes to annotate.
  • Use Organize actions to auto-layout, and Cmd+Z to undo.

Limits: 100 nodes, 200 edges, 50 notes per workflow.

Execution history

The Executions tab lists past runs with their status, runtime, trigger source, and per-node results, so you can audit what each fire actually did.

Maximum execution time: 5 minutes per workflow run.

Versioning

Edits auto-save and version automatically. While the workflow is in Draft, you can click Save as new version to create a named snapshot, and you can browse, preview, and restore prior versions from the version badge in the header.

Webhook signing

TofuPilot handles webhooks in two directions: inbound from connected providers such as GitHub, GitLab, and Linear, and outbound through the http_request action.

Outbound webhooks

The http_request action sends to a URL you configure, with {{variable}} placeholders interpolated in the URL, headers, and body. The Content-Type: application/json header is set by default.

Outbound HTTP requests are not signed. No HMAC, no timestamp, no nonce. Add your own check.

You have three options for authenticating the call on the receiving side:

  • Long random token in the URL path such as https://example.com/hooks/tofupilot/<token>, with mismatches rejected.
  • Custom Authorization: Bearer <token> header on the action, validated server-side.
  • Shared secret in the JSON body, checked before processing.

Inbound webhooks

TofuPilot verifies provider signatures using the secret stored on the installation, so you do not need a verifier on your side. You configure the secret on the provider's dashboard when installing the integration.

ProviderEndpointHeaderAlgorithm
GitHub/api/webhooks/githubx-hub-signature-256HMAC-SHA256 of raw body, hex, prefixed sha256=
GitLab/api/webhooks/gitlabx-gitlab-tokenShared secret matched against sha256(token)
Linear/api/webhooks/linearlinear-signatureHMAC-SHA256 of <timestamp>.<body>, formatted t=<timestamp>,s=<signature>

Each handler reads provider delivery headers for logging: x-github-delivery, x-github-event, x-gitlab-event, and x-gitlab-event-uuid.

Verifying a webhook signature

Below is a reference implementation for the Linear scheme TofuPilot uses inbound, which you can adapt to your stack.

verify_webhook.py
import hmac
import hashlib
import time

def verify(body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp = parts.get("t")
    signature = parts.get("s")
    if not timestamp or not signature:
        return False

    now_ms = int(time.time() * 1000)
    if abs(now_ms - int(timestamp)) > 5 * 60 * 1000:
        return False

    payload = f"{timestamp}.".encode() + body
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
verify-webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(body, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=', 2)),
  );
  const { t: timestamp, s: signature } = parts;
  if (!timestamp || !signature) return false;

  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) {
    return false;
  }

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  if (expected.length !== signature.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Always verify against the raw request body, because re-serializing the parsed JSON does not reproduce the signed bytes.

Replay protection

The Linear handler rejects any request whose timestamp is more than 5 minutes from server time. GitHub and GitLab inbound webhooks rely on the signature, and replay protection there comes from the provider's delivery id (x-github-delivery, x-gitlab-event-uuid).

For your own endpoints, apply the same 5-minute window once outbound signing is available. Until then, you can deduplicate by an idempotency key.

How is this guide?

On this page