Workflows
Workflows are trigger-driven automations that take a list of domains and run them through a configurable action graph. Triggers fire on events like a new intent alert, a watchlist change, an audience match, an inbound webhook, or a schedule. Actions cover the common moves — enroll into a sequence, add to a list, score, enrich, call an external API, branch on a condition, fan out into sub-workflows.
Workflows require the Starter plan or higher. Workflow plan limits: Starter 5 / Pro 25 / Business 100 / Enterprise unlimited.
Anatomy
Every workflow has four parts:
| Field | What it does |
|---|---|
trigger_type | When the workflow fires. One of intent_alert, audience_match, watchlist_change, new_domain, schedule, webhook, manual. |
trigger_config | Configuration specific to the trigger (e.g. {alert_id: 42} for intent_alert, {cron: "0 9 * * 1"} for schedule). |
conditions | Optional pre-filter applied to the candidate domains before any action runs. Array of {field, op, value} rules. |
actions | The action graph: an ordered list of {type, config} objects. Branches via parallel_split / merge_branches / conditional_branch. |
List Workflows
Endpoint
GET /v1/workflows
Example Request
curl -X GET \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/workflows"
Response
{
"workflows": [
{
"id": 17,
"name": "Notify Slack when fintech intent fires",
"description": "When a fintech intent alert matches, post the top 5 to #sales-leads.",
"trigger_type": "intent_alert",
"trigger_config": { "alert_id": 42 },
"conditions": [
{ "field": "industry", "op": "equals", "value": "fintech" }
],
"actions": [
{ "type": "filter_domains", "config": { "limit": 5 } },
{ "type": "http_request",
"config": {
"method": "POST",
"url": "https://hooks.slack.com/services/...",
"body": { "text": "Top 5 fintech: {{domains}}" }
}
}
],
"is_active": true,
"last_run_at": "2026-05-05T13:14:00Z",
"run_count": 28,
"created_at": "2026-04-12T11:00:00Z",
"updated_at": "2026-05-04T08:22:18Z"
}
],
"count": 1,
"limit": 25,
"remaining": 24
}
limit and remaining reflect plan-tier workflow quotas, not pagination.
Create Workflow
Endpoint
POST /v1/workflows
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–200 chars. |
description | string | No | Up to 2000 chars. |
trigger_type | string | Yes | intent_alert, audience_match, watchlist_change, new_domain, schedule, webhook, or manual. |
trigger_config | object | Sometimes | Required for triggers that need a target (alert_id, audience_id, etc.). |
conditions | array | No | Up to 50 condition objects. |
actions | array | Yes | Up to 100 action objects. |
execution_limits | object | No | {max_domains_per_run, max_runtime_seconds, ...} — overrides plan defaults. |
canvas_data | object | No | Visual editor coordinates. Free-form JSON, not interpreted by the engine. |
When trigger_type is webhook, the response includes a freshly-generated webhook_url and webhook_token — point your external system at the URL.
Example Request
curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Hot fintech to Slack",
"trigger_type": "intent_alert",
"trigger_config": { "alert_id": 42 },
"conditions": [{"field": "industry", "op": "equals", "value": "fintech"}],
"actions": [
{"type": "filter_domains", "config": {"limit": 5}},
{"type": "http_request", "config": {
"method": "POST",
"url": "https://hooks.slack.com/services/T00000/B00000/XXXXXXXX",
"body": {"text": "Top 5 fintech leads: {{domains}}"}
}}
]
}' \
"https://api.crondb.com/v1/workflows"
Get / Update / Delete Workflow
| Method | Path | Notes |
|---|---|---|
GET | /v1/workflows/{id} | Returns the workflow (with webhook URL when applicable). |
PUT | /v1/workflows/{id} | All fields optional. Each save creates a new entry in the version history (queryable at GET /workflows/{id}/versions). |
DELETE | /v1/workflows/{id} | Cascades to runs. |
Test (Dry Run)
Evaluate the trigger and conditions against sample domains and return what would execute, without actually running the action graph or charging budget.
POST /v1/workflows/{id}/test
Rate-limited to 10 calls per workflow per minute.
{
"workflow": { /* workflow object */ },
"test_results": {
"sample_domains_checked": 10,
"domains_after_conditions": 4,
"sample_filtered": [
{"domain": "stripe.com", "industry": "fintech", "intent_score": 91}
],
"actions_that_would_execute": [
{"type": "filter_domains", "config": {"limit": 5}},
{"type": "http_request", "config": {"method": "POST", "url": "..."}}
]
}
}
Execute Now
Run a workflow immediately against the trigger's natural domain source. Unlike /test, this charges budget and writes a WorkflowRun row. Use for manual reruns or manual-trigger workflows.
POST /v1/workflows/{id}/execute?activate=false
Set activate=true to additionally flip is_active=true if the workflow was paused.
List Runs
GET /v1/workflows/{id}/runs?limit=20&offset=0
Returns recent execution history with status (success, error, partial), trigger event, domain count, action results, and start/end timestamps. Each run row carries actions_executed — a JSONB array with one entry per action including its status, result, and any captured fields. Bearer tokens used in http_request headers are redacted before persisting.
Stream a single run
GET /v1/workflows/{id}/runs/{run_id}/stream
Server-Sent Events stream that emits action-by-action progress as the run executes. Each event is a JSON payload with the action type, status, and incremental result. Useful for live UIs.
Action graph reference
Every action object has the shape {type, config, retry?}. Common types:
| Type | What it does |
|---|---|
enroll_in_sequence | {sequence_id} — Enroll the current domain set into a sequence. |
add_to_list | {list_id} — Push to a saved lead list. |
score_domains | {rule_id} — Apply a scoring rule and stamp the score onto each domain. |
enrich | {fields: ["dns","ssl","whois","tech_stack"]} — Fan out enrichment. |
filter_domains | {op, field, value, limit} — Narrow the domain list. |
data_transform | {operation, ...} — map_fields, sort, dedup, limit, slice. |
http_request | Call any external API. See below. |
conditional_branch | {branches: [{when, then}, ...]} — Route to different sub-actions. |
parallel_split / merge_branches | Run sub-graphs in parallel, then re-merge. |
cross_run_memory | {op: "store"|"recall", key, value} — Persist state across runs. |
wait_for_approval | {slack_url|email, message, expires_in_hours} — Pause until a human clicks Approve/Reject. |
auto_reply_template | Auto-reply to inbox threads using a stored template. |
schedule_workflow | Trigger another workflow now or at a future time. |
sub_workflow | {workflow_id} — Inline another workflow's actions. Cycle detection guards against A→B→A. |
http_request action
The most flexible action — call any external API and capture the response.
{
"type": "http_request",
"config": {
"method": "POST",
"url": "https://api.example.com/v1/leads/{{domain}}",
"headers": { "Authorization": "Bearer {{ai_memory_recall:example_token}}" },
"body": { "domain": "{{domain}}", "score": "{{intent_score}}" },
"timeout_seconds": 30,
"capture_field": "data.id",
"iterate_into_domains": false
},
"retry": { "attempts": 2, "backoff_seconds": 1.0 }
}
| Config field | Description |
|---|---|
method | GET, POST, PUT, PATCH, or DELETE. |
url | Target URL. Supports {{variable}} template tokens, including {{ai_memory_recall:KEY}}. |
headers | Object of header name → value. Authorization headers are redacted in actions_executed audit trail. |
body | JSON body for POST/PUT/PATCH. Templated. |
timeout_seconds | Default 30. |
capture_field | Dot-path into the response JSON. The captured value is exposed to downstream actions as {{previous_result}}. |
iterate_into_domains | When capture_field resolves to a list, replace the working domains set with those items so downstream actions fan out per item. Strings become {"domain": str}; objects pass through unchanged. The n8n "Item Lists" pattern. |
SSRF guard
The http_request action blocks targets in private IP ranges, link-local + loopback, IPv6 unique-local, and known cloud metadata endpoints (169.254.169.254 and friends). Calls to those targets return a Blocked by SSRF guard error without making the network request.
Per-action retry
Add a retry block at the action level (sibling of config) to retry transient failures with exponential backoff:
"retry": { "attempts": 2, "backoff_seconds": 0.5 }
| Field | Range | Description |
|---|---|---|
attempts | 0–5 | Number of additional attempts after the first failure. |
backoff_seconds | 0–30 | Base delay. The engine doubles the wait between attempts (backoff × 2^(n-1)). |
Set attempts: 0 (or omit retry) to keep the previous "fail fast" behavior. The retry_failed flag at the workflow level keeps working as a global "rerun the entire workflow once" knob; the per-action retry is independent and stacks on top.
Webhook trigger
Workflows with trigger_type: "webhook" get an auto-generated public URL. POST any JSON to it to fire the workflow.
POST https://api.crondb.com/v1/workflows/incoming/{token}
The token IS the auth — keep it secret. Rate-limited to 30 calls per token per minute.
curl -X POST \
-H "Content-Type: application/json" \
-d '{"domains": ["acme.com", "globex.com"], "source": "my-crm"}' \
"https://api.crondb.com/v1/workflows/incoming/abc123def456..."
The body is passed as trigger_data. Any domains key is extracted and used as the action input — accept either an array of strings or an array of objects ({domain, industry, ...}). When domains is omitted, the engine seeds with a sample query against the domain pool.
Response:
{
"status": "accepted",
"workflow_id": 17,
"run_id": 1042,
"run_status": "success"
}
Token management
| Method | Path | Description |
|---|---|---|
GET | /v1/workflows/{id}/webhook-token | Reveal the token + URL + trigger count. |
POST | /v1/workflows/{id}/webhook-token/regenerate | Rotate. The old token immediately stops working. |
Approvals
Pause a workflow mid-run until a human approves or rejects. Use the wait_for_approval action to send a Slack/email message with an Approve/Reject link, then resume execution with the decision available as {{trigger.approval_decision}} on the spawned follow-up run.
Approval landing
GET /v1/workflows/approvals/{token}
Public endpoint (no auth — the token is the secret). Renders an HTML page with Approve / Reject buttons that POST back to the same path. Use this URL in the Slack/email body of wait_for_approval.
Record decision
POST /v1/workflows/approvals/{token}
Accepts both JSON ({"decision": "approved"|"rejected"}) and form-encoded (decision=approved). On approved or rejected, the engine dispatches a NEW workflow run with trigger_event=approval_received carrying the decision — branch on it via conditional_branch on {{trigger.approval_decision}}.
Returns {status: "approved"|"rejected"|"already_decided"|"expired", ...}. Approvals expire automatically (default 24h, configurable in the action's expires_in_hours).
Budget
Cap monthly LLM spend per workflow. The engine debits each AI call (via ai_extract, ai_write, auto_reply_template, etc.) and auto-pauses the workflow when the cap is hit.
| Method | Path | Description |
|---|---|---|
GET | /v1/workflows/{id}/budget | Current cap + month-to-date spend. Returns configured: false if no cap set. |
PUT | /v1/workflows/{id}/budget | Set or update. Body: {"monthly_cap_usd": 25.00, "auto_pause_on_exceed": true}. |
POST | /v1/workflows/{id}/budget/reset | Zero current month spend (manual override of the auto-pause). |
DELETE | /v1/workflows/{id}/budget | Remove the cap entirely. |
Setting monthly_cap_usd: 0 disables the guard — the engine treats 0 as "no cap." Spend resets automatically at the start of every month; the workflow_maintenance_worker does the rollover.
Saved Connections
Reuse API endpoints across many http_request actions without re-typing the URL/auth in each one.
| Method | Path | Description |
|---|---|---|
GET | /v1/connections | List saved connections. |
POST | /v1/connections | Create. Body: {name, base_url, headers, auth_type, description}. Max 20 per user. |
PUT | /v1/connections/{id} | Update any field. |
DELETE | /v1/connections/{id} | Delete. Doesn't break workflows that referenced it — they fall back to the inline URL. |
auth_type is one of none, bearer, basic, api_key. The actual secret lives encrypted in the headers blob, not on the row itself.
Versioning
Every PUT /v1/workflows/{id} snapshots the previous state to workflow_versions. Use these endpoints to inspect history or roll back:
| Method | Path |
|---|---|
GET | /v1/workflows/{id}/versions |
GET | /v1/workflows/{id}/versions/{version_id} |
POST | /v1/workflows/{id}/versions/{version_id}/rollback |
Templates & Marketplace
Pre-built workflows you can install with one click. Discoverable at GET /v1/workflows/templates (filterable by detail=summary|full). Install with POST /v1/workflows/install-template (body: {template_id}). Browse community-published workflows at GET /v1/workflows/marketplace. See Workflow Templates for the dashboard guide.
Notes
- Workflow execution is in-process async. A redeploy mid-run will lose any actively-executing workflow; the
workflow_run_queuetable is the foundation for durable execution but the broader migration is on the roadmap. - All AI calls inside actions are logged to the AI cost dashboard with
action_typeof the originating action (e.g.workflow:ai_extract,workflow:auto_reply_template). - The
actions_executedJSONB on eachWorkflowRunis your audit trail — it captures every action's input, output, status, and timing. Headers inhttp_requestare redacted to keep tokens out of the audit log. - A
workflow_maintenance_workerruns hourly: prunes runs older than 90 days (configurable), resets monthly budgets, expiresai_memorykeys past their TTL.
Next Steps
- Sequences — Most common downstream action: enroll a workflow's domain set into an outreach sequence.
- Webhooks — Outbound webhooks complement workflows. Workflows fire actions; outbound webhooks notify your stack of CronDB-side events.
- Workflow Builder — Visual editor walkthrough.