When it runs, and what it runs for.
- Trigger
trigger - Run on demand, or on a daily, weekly or monthly schedule in your time zone.
- Fan out
fan_out - Repeat the steps after it for up to 25 entities. Write {{entity}} in a question to use each one.
Build with it / Research pipelines
Chain research runs, checks, approvals and deliveries, then let them run on a schedule. Build a pipeline in the visual builder, through the API, or let your AI agent run it over MCP.
01TriggerOn demand or on a schedule.
02ResearchOne question, or one per entity.
03DecideA change check, a readiness score, a person.
04DeliverPDF, notification or your webhook.
WHY A PIPELINE
Some questions need asking again and again. Prices move, rules change, competitors ship.
A pipeline asks for you on schedule, compares each answer with the last one, and only bothers anyone when something real changed. When it does, a person can sign off before the result goes anywhere, and then it lands where your team works: a PDF, a notification, or your own system through a webhook.
01 / AN EXAMPLE
Every Monday it researches three competitors' pricing, keeps going only for the ones whose answer moved, waits for a sign-off, then sends a PDF and posts the report to a CRM.
Only research steps use credits: three Standard runs a week here. The condition, the approval and the deliveries are free, and validation tells you the ceiling before anything runs.
02 / THE STEPS
Connect them in a line, branch on a condition, or fan out across a list. Steps that work on a report need a research step earlier on their branch, and the builder tells you when one is missing.
When it runs, and what it runs for.
triggerfan_outThe steps that do the work, and the only ones that use credits.
researchextendrabbit_holedossierStop the line until the evidence or a person says go.
conditionapprovalSend the result where it gets used.
exportnotifywebhook03 / THREE WAYS TO BUILD
A pipeline is one graph of steps. The builder draws it, the API takes it as JSON, and MCP lets your agent run and follow it. All three see the same pipelines.
Drag steps onto a canvas, connect them, and see the credit ceiling update as you go. Runs, approvals and step outputs show up live.
Open the builderValidate a graph for free, save it, start runs with an idempotency key and read every step's result.
API referenceClaude, Cursor or VS Code can list your pipelines, run one, follow it and answer approvals.
Connect over MCP# Put one of your endpoint ids (GET /v1/webhooks) in place of YOUR_WEBHOOK_ENDPOINT_ID first.
curl -X POST https://api.farsideresearch.com/v1/workflows/validate \
-H "Authorization: Bearer $FARSIDE_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"graph": {
"nodes": [
{
"id": "weekly",
"type": "trigger",
"config": {
"mode": "schedule",
"cadence": "weekly",
"timezone": "America/New_York"
}
},
{
"id": "competitors",
"type": "fan_out",
"config": {
"items": [
"Acme Analytics",
"Northwind Data",
"Contoso Cloud"
]
}
},
{
"id": "prices",
"type": "research",
"config": {
"source": "inline",
"question": "What does {{entity}} charge for its analytics plans in 2026, and what changed recently?",
"depth": "standard",
"research_type": "competitive_intel",
"compare_to_previous": true
}
},
{
"id": "moved",
"type": "condition",
"config": {
"check": "material_change",
"threshold": 0.15,
"on_first_run": "pass"
}
},
{
"id": "signoff",
"type": "approval",
"config": {
"instructions": "Check the price change before it goes to sales.",
"expires_after_hours": 72
}
},
{
"id": "pdf",
"type": "export",
"config": {
"format": "pdf"
}
},
{
"id": "crm",
"type": "webhook",
"config": {
"endpoint_id": "YOUR_WEBHOOK_ENDPOINT_ID"
}
}
],
"edges": [
{
"id": "e1",
"source": "weekly",
"target": "competitors"
},
{
"id": "e2",
"source": "competitors",
"target": "prices"
},
{
"id": "e3",
"source": "prices",
"target": "moved"
},
{
"id": "e4",
"source": "moved",
"target": "signoff",
"branch": "pass"
},
{
"id": "e5",
"source": "signoff",
"target": "pdf"
},
{
"id": "e6",
"source": "signoff",
"target": "crm"
}
]
}
}
JSON
# {"valid": true, "issues": [], "estimate": {"max_credits": ..., "research_runs": 3, "breakdown": [...]}}curl -X POST https://api.farsideresearch.com/v1/workflows \
-H "Authorization: Bearer $FARSIDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Weekly competitor price watch", "status": "active", "graph": { ...same graph... }}'curl -X POST https://api.farsideresearch.com/v1/workflows/$WORKFLOW_ID/executions \
-H "Authorization: Bearer $FARSIDE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"
# Retry with the same Idempotency-Key and you get the same execution back.curl -X POST https://api.farsideresearch.com/v1/workflow-executions/$EXECUTION_ID/steps/$STEP_ID/decision \
-H "Authorization: Bearer $FARSIDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"decision": "approve", "note": "Price change confirmed with sales."}'/v1/workflow-catalog/v1/workflows/validate/v1/workflows/v1/workflows/{id}/executions/v1/workflow-executions/{id}/v1/workflow-executions/{id}/steps/{step_id}/decision/v1/workflow-approvalslist_workflowsrun_workflowget_workflow_executiondecide_workflow_approval04 / GUARDRAILS
A pipeline spends credits and sends things out of Farside, so the limits are part of the engine, not a setting you have to remember.
Subscribe an endpoint to pipeline events, or add a webhook step to post a specific result. Every delivery carries X-Farside-Event, X-Farside-Delivery-Id and an X-Signature you can check. X-Farside-Delivery and X-Farside-Timestamp are included too. Deprecated X-Fathom-* aliases remain available until at least 2027-06-01.
workflow.stepworkflow.completedworkflow.failedworkflow.approval_requestedimport hashlib, hmac, time
def verify(secret: str, body: bytes, signature_header: str, tolerance: int = 300) -> bool:
# X-Signature looks like: t=1762700000,v1=<hex sha256>
parts = dict(item.split("=", 1) for item in signature_header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False
signed = str(timestamp).encode() + b"." + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])