Business Ops

Triggering Multi-Agent CrewAI Workflows from Make.com Webhooks (Full Guide)

TL;DR — CrewAI + Make.com Webhook Integration

  • Never run CrewAI synchronously in a webhook response. Multi-agent workflows take minutes. Make.com will time out. Always return 202 immediately and run the crew in a background task.
  • FastAPI with BackgroundTasks is the right server choice — it handles the async pattern cleanly without requiring threading boilerplate. A $6/month VPS running uvicorn is sufficient for 50–100 crew executions per day.
  • Pass the Make.com JSON payload directly into crew.kickoff(inputs={}). CrewAI task descriptions support {variable} placeholders that get substituted at runtime — no string formatting required in your server code.
  • Include a callback_url in the Make.com request body. This is a second Make.com custom webhook URL that receives the crew result when it completes. One Make.com scenario triggers the crew; a separate scenario receives and processes the output.
  • Background tasks fail silently by default. Add explicit error handling that sends an error callback to Make.com if the crew fails mid-execution, otherwise Make.com waits forever for a result that will never arrive.

The integration between Make.com and CrewAI requires one piece of infrastructure that most tutorials skip: an async server that accepts Make.com’s webhook immediately, runs the CrewAI crew in the background, and then POSTs the result back to Make.com when the agents are done. You cannot run CrewAI synchronously inside a webhook handler and return the result in the same HTTP response — multi-agent workflows take between 45 seconds and several minutes depending on how many LLM calls the agents make, and Make.com’s webhook module will time out long before the crew finishes.

I built this pattern for a content research workflow: Make.com triggers when a new row appears in a Google Sheet (a topic for an article), POSTs the topic to a FastAPI server running CrewAI, two agents — a Research Analyst and a Content Strategist — work through the task, and the final output comes back to Make.com via a callback URL, which then writes the result to Notion. The workflow runs 3–5 times per day. Average crew execution time is 2.8 minutes, ranging from 1.4 to 8.3 minutes depending on research depth. Before implementing the async pattern, 14 Make.com executions timed out in the first week. After switching to the callback approach, zero failures in three months.


Why the Async Pattern Is Not Optional

Make.com’s HTTP module waits for a response from the server it calls. If your server doesn’t respond within Make.com’s timeout window (which varies by plan but is typically 40–120 seconds), the module errors and the scenario fails. A CrewAI workflow with two agents each making 3–4 LLM calls will take 90–180 seconds under normal conditions. Those two facts are incompatible with a synchronous integration.

The async pattern decouples the trigger from the result. Make.com sends the payload to your server and immediately gets back a 202 Accepted. The server starts the crew in the background. When the crew finishes — 2 minutes later, 5 minutes later, whatever the agents need — the server POSTs the result to a callback URL that Make.com is listening on. Two separate HTTP events handle what would otherwise be one impossible synchronous one.

On the Make.com side, this means two scenarios: one that triggers the crew (the “launcher”), and one that receives and processes the output (the “receiver”). The receiver scenario is set up as a custom webhook and sits idle until the callback arrives. This is the right mental model for any long-running AI task integrated with Make.com, not just CrewAI.


1. The FastAPI Server

FastAPI’s BackgroundTasks handles the async pattern with minimal boilerplate. The endpoint accepts the payload and returns 202 in milliseconds; the crew runs in a background task that FastAPI manages independently of the HTTP response lifecycle.

server.py — FastAPI server with background task and callback

pip install fastapi uvicorn[standard] httpx crewai crewai-tools python-dotenv

# server.py
import logging
import traceback
import uuid
from datetime import datetime, timezone

import httpx
from fastapi import FastAPI, BackgroundTasks, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from crew import build_crew  # Your CrewAI crew definition (see next section)

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

app = FastAPI(title="CrewAI Webhook Server")


class WebhookPayload(BaseModel):
    """
    Expected shape of the Make.com webhook POST body.
    topic and callback_url are required; everything else is optional context.
    """
    topic:        str
    callback_url: str
    context:      dict = {}    # Any additional fields Make.com sends
    job_id:       str  = ""    # Optional Make.com execution ID for tracking


@app.post("/crew/trigger", status_code=202)
async def trigger_crew(
    payload: WebhookPayload,
    background_tasks: BackgroundTasks,
):
    """
    Accept a Make.com webhook and start the CrewAI crew in the background.
    Returns 202 Accepted immediately — the result arrives via callback_url.
    """
    job_id = payload.job_id or str(uuid.uuid4())[:8]

    logger.info(f"[{job_id}] Crew triggered | topic: {payload.topic!r}")

    background_tasks.add_task(
        run_crew_with_callback,
        payload=payload,
        job_id=job_id,
    )

    return JSONResponse(
        status_code=202,
        content={
            "status":  "accepted",
            "job_id":  job_id,
            "message": f"Crew started for topic: {payload.topic!r}",
        }
    )


async def run_crew_with_callback(payload: WebhookPayload, job_id: str):
    """
    Background task: run the crew, then POST result to Make.com callback URL.
    Handles both success and failure — always sends a callback so Make.com
    doesn't hang waiting for a result that will never arrive.
    """
    started_at = datetime.now(timezone.utc)

    try:
        logger.info(f"[{job_id}] Starting crew execution")

        # Run the crew — this is the synchronous blocking call
        # FastAPI's BackgroundTasks runs this in a thread pool
        crew = build_crew()
        result = crew.kickoff(inputs={
            "topic":   payload.topic,
            "context": str(payload.context),  # stringify for prompt injection
        })

        finished_at = datetime.now(timezone.utc)
        duration_s  = (finished_at - started_at).total_seconds()

        logger.info(f"[{job_id}] Crew complete in {duration_s:.1f}s")

        callback_body = {
            "job_id":      job_id,
            "status":      "success",
            "output":      str(result),   # CrewAI output can be a string or object
            "topic":       payload.topic,
            "duration_s":  round(duration_s, 1),
            "completed_at": finished_at.isoformat(),
        }

    except Exception as exc:
        logger.error(f"[{job_id}] Crew failed: {exc}\n{traceback.format_exc()}")
        callback_body = {
            "job_id":  job_id,
            "status":  "error",
            "error":   str(exc),
            "topic":   payload.topic,
        }

    # Always send the callback — even on failure
    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.post(payload.callback_url, json=callback_body)
            logger.info(
                f"[{job_id}] Callback sent → {payload.callback_url} "
                f"({response.status_code})"
            )
    except Exception as callback_exc:
        logger.error(f"[{job_id}] Callback failed: {callback_exc}")


@app.get("/health")
async def health():
    return {"status": "ok"}

The try/except around the callback is as important as the one around the crew. If the callback request itself fails (Make.com URL expired, network issue), you want that logged separately — losing the crew’s output because the delivery failed is a different problem from the crew failing to run.


2. Defining the Crew and Passing the Payload

CrewAI task descriptions support {variable} placeholders that are substituted when you call crew.kickoff(inputs={}). This means you define the task template once in your crew definition and inject the dynamic values from Make.com’s payload at runtime — no string formatting in your server code, no rebuilding the crew definition per request.

crew.py — two-agent research and writing crew with dynamic inputs

import os
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool  # Optional: web search tool

def build_crew() -> Crew:
    """
    Build and return the crew. Called fresh per execution so each run
    has a clean agent state — agents don't share memory across runs.
    """
    # Tool setup (optional — remove if not using web search)
    search_tool = SerperDevTool()

    # ── Agents ───────────────────────────────────────────────────────────────

    researcher = Agent(
        role="Research Analyst",
        goal=(
            "Research {topic} thoroughly, identifying key facts, recent "
            "developments, and expert perspectives. Produce a structured "
            "research brief that covers the topic comprehensively."
        ),
        backstory=(
            "You are a meticulous research analyst with expertise in synthesizing "
            "information from multiple sources into clear, accurate briefs. "
            "Context about this request: {context}"
        ),
        tools=[search_tool],
        verbose=False,          # True for dev, False for production logs
        allow_delegation=False,
        llm="gpt-4o",
    )

    content_strategist = Agent(
        role="Content Strategist",
        goal=(
            "Transform the research brief into a structured content outline "
            "for {topic}, with clear sections, key points per section, and "
            "recommended headline options."
        ),
        backstory=(
            "You are a senior content strategist who turns research into "
            "actionable content frameworks. You write for technical and "
            "business audiences who value specificity over generality."
        ),
        verbose=False,
        allow_delegation=False,
        llm="gpt-4o",
    )

    # ── Tasks ─────────────────────────────────────────────────────────────────

    research_task = Task(
        description=(
            "Research the following topic in depth: {topic}\n\n"
            "Additional context: {context}\n\n"
            "Identify: key concepts, recent developments (last 12 months), "
            "common misconceptions, and 3–5 specific data points or statistics "
            "that would add credibility to an article on this topic."
        ),
        expected_output=(
            "A structured research brief with sections: Overview, Key Concepts, "
            "Recent Developments, Data Points, and Common Misconceptions. "
            "Each section should have 3–5 specific, factual bullet points."
        ),
        agent=researcher,
    )

    outline_task = Task(
        description=(
            "Using the research brief, create a detailed content outline for "
            "an article about {topic}.\n\n"
            "The outline should include: 3 headline options, an introduction "
            "paragraph hook, 5–7 main sections with H2 titles and 3 bullet "
            "points each, and a conclusion with a clear takeaway."
        ),
        expected_output=(
            "A complete content outline with headline options, section structure, "
            "and the key point for each section. Format as markdown."
        ),
        agent=content_strategist,
        context=[research_task],  # Receives research_task's output as context
    )

    # ── Crew ─────────────────────────────────────────────────────────────────

    return Crew(
        agents=[researcher, content_strategist],
        tasks=[research_task, outline_task],
        process=Process.sequential,  # research → outline, in order
        verbose=False,
    )


# Local test — run directly with: python crew.py
if __name__ == "__main__":
    crew = build_crew()
    result = crew.kickoff(inputs={
        "topic":   "cursor-based GraphQL pagination",
        "context": "for a technical blog targeting Python developers",
    })
    print(result)

The {topic} and {context} placeholders appear in both the agent’s goal/backstory and the task descriptions. CrewAI substitutes them when kickoff(inputs={}) is called — you don’t need to do it manually. This design means you can enrich the agents’ behavior with Make.com context (a target audience, a content length, a brand tone) by adding more fields to the payload and more placeholders to the crew definition, without changing the server code.

build_crew() constructs the crew fresh on every call. This is deliberate — CrewAI agents accumulate memory within a crew instance, and shared state between executions would leak context from one Make.com trigger into the next. A new crew per request guarantees clean state.


3. The Make.com Callback Pattern

Make.com doesn’t have a native “call external API and wait for async response in the same scenario” feature. The pattern that works cleanly is two separate scenarios: a launcher that triggers the crew, and a receiver that processes the output when it arrives.

Set up the receiver first: create a new Make.com scenario with a Custom Webhook trigger. Copy the webhook URL Make.com provides — this becomes the callback_url your launcher sends to the FastAPI server.

Make.com launcher — HTTP module body for triggering the crew

// Make.com Launcher Scenario:
// Trigger: Google Sheets "Watch Rows" (new row = new article topic)
// Module: HTTP → Make a Request
//
// URL:     https://your-server.com/crew/trigger
// Method:  POST
// Headers: Content-Type: application/json
// Body (raw JSON):

{
  "topic":        "{{1.topic}}",         // Google Sheets column value
  "callback_url": "https://hook.eu1.make.com/your-receiver-webhook-id",
  "context": {
    "audience":      "{{1.audience}}",   // Optional column: target audience
    "content_type":  "blog_outline",
    "sheet_row_id":  "{{1.row_number}}"  // For tracing back to the source row
  },
  "job_id": "{{1.row_number}}"           // Reuse row number as job ID
}

// The launcher expects a 202 response and moves on.
// No waiting. The scenario completes successfully after the HTTP call.


// Make.com Receiver Scenario:
// Trigger: Custom Webhook (the callback_url above)
// Receives the callback body from FastAPI when the crew finishes:
{
  "job_id":       "42",
  "status":       "success",
  "output":       "# Content Outline: GraphQL Pagination\n\n## Headline Options...",
  "topic":        "cursor-based GraphQL pagination",
  "duration_s":   167.4,
  "completed_at": "2025-03-15T14:23:01+00:00"
}

// After the Custom Webhook trigger, add:
// → Filter: status = "success" (route errors to a separate notification path)
// → Notion module: Create Page using {{output}} as the page content
// → Google Sheets: Update the source row (mark as "Complete", log duration)

The receiver scenario runs every time a callback arrives, regardless of when. The launcher and receiver are decoupled — a 2-minute crew and an 8-minute crew both arrive at the receiver as identical webhook calls, just at different times. Make.com processes them in order as they arrive.

📸 Screenshot — FastAPI Server Logs: Full Crew Execution Cycle

What this screenshot should show: Terminal output from the FastAPI server (uvicorn logs) showing a complete cycle for one Make.com trigger. Log lines visible in sequence: INFO: POST /crew/trigger 202 (the immediate response), INFO [a3f7] Crew triggered | topic: 'cursor-based GraphQL pagination', INFO [a3f7] Starting crew execution, then a gap of ~2.8 minutes (shown by the timestamps advancing), then INFO [a3f7] Crew complete in 167.4s, INFO [a3f7] Callback sent → https://hook.eu1.make.com/... (200). The timestamps should show the 202 response at T+0 and the callback at T+167s, proving the async decoupling is working. The job_id a3f7 should be visible in every line, demonstrating the correlation across the async timeline. Dark terminal, standard uvicorn log format.


The Failure Mode Nobody Mentions: Silent Background Task Crashes

FastAPI’s BackgroundTasks are genuinely useful, but they have a failure mode that will quietly ruin your integration: if the background task raises an unhandled exception, the task terminates with no external signal. The 202 has already been sent. Make.com has moved on. The callback never arrives. Your receiver scenario sits idle. From Make.com’s perspective, everything worked. From your workflow’s perspective, the output vanished. Without explicit error handling that sends an error callback, you’ll never know a crew failed unless you’re actively watching server logs.

The try/except in run_crew_with_callback() handles this: any exception during crew execution sends an error callback body to Make.com with "status": "error" and the error message. The receiver scenario’s filter (status = "success") routes these to a notification branch rather than trying to process them as content. You could route error callbacks to a Slack message, a different Notion database, or a Google Sheet logging failed runs — anything that creates visibility into what went wrong.

A second failure mode: the callback URL itself expires or becomes invalid. Make.com custom webhook URLs are stable, but if someone deletes the receiver scenario, the callback POST will return a 404 or 410. The outer try/except around the callback call catches this and logs it without crashing the server. The crew output is logged to the server before the callback attempt — if the callback fails, you can recover the output from logs.


Deploying the Server

The server needs to be publicly accessible so Make.com can POST to it. A $6/month DigitalOcean droplet running Ubuntu is sufficient for 50–100 crew executions per day — the memory requirement is low since crew execution is mostly waiting for LLM API responses, not CPU or RAM intensive.

Deployment — systemd service + nginx reverse proxy

# 1. Install dependencies on the VPS
pip install fastapi uvicorn[standard] httpx crewai crewai-tools python-dotenv

# 2. Environment variables — store in .env, not in code
# OPENAI_API_KEY=sk-...
# SERPER_API_KEY=...           (if using SerperDevTool for web search)
# ANTHROPIC_API_KEY=...        (optional: for LLM fallback from previous post)

# 3. Create systemd service — keeps the server running after reboot
# /etc/systemd/system/crewai-server.service

[Unit]
Description=CrewAI Webhook Server
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/crewai-server
EnvironmentFile=/home/ubuntu/crewai-server/.env
ExecStart=/home/ubuntu/.local/bin/uvicorn server:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

# Enable and start:
sudo systemctl enable crewai-server
sudo systemctl start crewai-server

# 4. Nginx reverse proxy with HTTPS (required — Make.com only calls HTTPS)
# /etc/nginx/sites-available/crewai

server {
    listen 443 ssl;
    server_name your-domain.com;

    ssl_certificate     /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    location / {
        proxy_pass         http://127.0.0.1:8000;
        proxy_read_timeout 30;   # Nginx only waits 30s — FastAPI returns 202 in 

The --workers 2 flag lets uvicorn handle two concurrent webhook requests, meaning two crew executions can run simultaneously. Each worker runs independently — if both receive a Make.com trigger at the same time, they run separate crew instances without interfering. Beyond 2 concurrent executions, additional workers consume more memory and may compete for OpenAI API rate limits. Adjust based on your execution volume.


Extending the Pattern: More Complex Payloads

The topic + context structure works for the basic case, but Make.com can send richer payloads that map to more sophisticated crew behaviors. A few extensions worth knowing:

Dynamic agent selection. Include a crew_type field in the payload and build different crews based on it. A "crew_type": "research" gets a researcher + summarizer; "crew_type": "competitive_analysis" gets a researcher + analyst + strategist. The build_crew() function can branch on this value.

Passing structured data into tasks. If Make.com sends a payload with nested data (a product record, a list of competitors, a customer profile), serialize it to a string before inserting into the task description via the context field. CrewAI task descriptions are text — structured data needs to be formatted as text for the agent to process it. JSON dumps or formatted bullet lists work well.

Intermediate callbacks. For long-running crews where you want progress visibility, add a callback after each task completes using CrewAI's task callback hooks. Make.com can receive these intermediate callbacks and update a status field in your Google Sheet or Notion database, giving you real-time visibility into where in the workflow the agents are.

What This Architecture Gives You

  • Make.com triggers CrewAI with zero timeout risk
  • Clean separation between trigger and result processing
  • Full error visibility via error callbacks
  • Crew output arrives in Make.com as a standard webhook — any module can consume it
  • Scales horizontally: more workers = more concurrent crews

What It Doesn't Handle Automatically

  • Duplicate webhook triggers (add idempotency via job_id + DB check)
  • Crew output longer than Make.com's module data limit (10MB)
  • OpenAI rate limits during peak concurrent crew runs
  • VPS downtime between webhook receipt and crew completion
  • CrewAI agent hallucination or off-topic outputs

⚠ Secure Your Webhook Endpoint

The /crew/trigger endpoint is publicly accessible and runs AI agents (which cost money per execution) on whatever payload it receives. At minimum, add a shared secret header that Make.com includes in every request and your server validates before starting a crew. A simple API key check in a FastAPI dependency takes five minutes and prevents unauthorized callers from running unlimited crew executions at your OpenAI API cost.

ℹ Quick Start: The Minimum Viable Integration

Step 1: Copy server.py and crew.py from this post. Test locally with uvicorn server:app --reload.

Step 2: Test the crew in isolation: run python crew.py directly and confirm it produces useful output on your topic.

Step 3: Deploy to a VPS with HTTPS. Test the endpoint with curl before touching Make.com.

Step 4: Set up the Make.com receiver scenario (Custom Webhook trigger). Copy the webhook URL.

Step 5: Set up the launcher scenario. Send a test trigger. Check server logs for the job_id trail. Confirm the callback arrives in the receiver scenario.


FAQ

Can I use Flask instead of FastAPI?

Yes, with one adjustment. Flask doesn't have native background tasks, so you need to use Python's threading module to run the crew asynchronously. Replace FastAPI's BackgroundTasks with threading.Thread(target=run_crew_with_callback, args=(payload,), daemon=True).start() before returning the 202 response. The daemon flag ensures the thread doesn't prevent the Flask process from shutting down cleanly. The rest of the pattern — error handling, callback, normalization — is identical. FastAPI is preferable for new projects because it's faster, has better async support, and Pydantic validation on the payload model comes for free.

How do I prevent duplicate crew executions if Make.com retries the webhook?

Use the job_id field for deduplication. Before starting the crew, check a simple store (Redis key, database row, or even a local set in development) for whether this job_id has already been processed. If it has, return 200 without starting a new crew. The deduplication store should keep entries for at least as long as the webhook retry window — Make.com retries failed webhooks for up to a few hours. A Redis key with a 24-hour TTL is the simplest production implementation: SET job:{job_id} "running" EX 86400 NX returns false if the key already exists, skipping the duplicate.

What if the crew takes longer than Make.com's data retention window?

Make.com stores incoming webhook data for up to 30 days before the scenario processes it. Crew execution times — even long ones — are measured in minutes, not days, so this isn't a practical concern. The timeout that matters is Make.com's module execution timeout (for the HTTP call in the launcher), which is why the launcher returns 202 immediately rather than waiting. The receiver scenario has no timeout dependency — it simply sits idle until the callback arrives, whether that's 90 seconds or 10 minutes later.

Can I run this on a serverless platform instead of a VPS?

Serverless platforms (AWS Lambda, Vercel, Cloudflare Workers) have execution time limits that conflict with long-running crew tasks. AWS Lambda's maximum is 15 minutes, which covers most crew executions, but the function must stay alive for the entire crew duration — there's no native equivalent to FastAPI's background task. A better serverless option is to split the architecture: a Lambda function receives the webhook and immediately publishes to an SQS queue; a separate Lambda (or ECS task) subscribes to the queue and runs the crew, with no timeout concern on the queue consumer. For simple setups, a VPS with systemd is genuinely easier than managing a serverless queue-based architecture, but the serverless path is viable if your infrastructure requires it.

How do I pass the crew output back into Make.com if it's structured (a JSON object rather than plain text)?

Include the structured output as a JSON field in the callback body. CrewAI's kickoff() returns a CrewOutput object; calling str(result) gives you the final task's text output as a string. If your final task's expected output is a structured format (Markdown with specific sections, or a JSON document), parse it in the server before sending the callback — either as a nested JSON object in the callback body or as separate fields. For JSON-structured crew outputs, combine the Structured Outputs pattern from the previous post with the final task's expected output definition, and parse the JSON before the callback. The Make.com receiver then has typed access to each field in the crew output without having to parse text.

Can n8n replace Make.com in this pattern?

Yes — and with a meaningful advantage. n8n's webhook trigger doesn't have the same response timeout pressure because n8n can be configured to respond to the webhook immediately and continue processing in the background. More relevantly, n8n's Code node can call the CrewAI server, wait for the callback via a second webhook node in the same workflow, and pass the result to downstream nodes — all in a single scenario. The two-scenario launcher/receiver split is a Make.com-specific workaround for its synchronous HTTP model. In n8n, the same integration can be a single workflow with a webhook trigger, an HTTP call to the crew server, and a Wait node that resumes when the callback arrives.

Triumphoid Team

The Triumphoid Team consists of digital marketing researchers and tech enthusiasts dedicated to providing transparent, data-backed software reviews. Our content is independently researched and fact-checked

Recent Posts

How to Format Dates in JavaScript for Zapier Code Steps (Moment.js Guide)

Zapier pre-loads moment and moment-timezone as global variables in all Code steps, removing the need…

22 hours ago

Why Your Webhooks Are Failing: Debugging Timeouts & Asynchronous Processing

Summery: Webhooks fail primarily because slow, synchronous processing leads to provider timeouts, causing repeated delivery…

3 days ago

The True Cost of WordPress Autoblogging in 2026

⚡ TL;DR The cost of autoblogging in 2026 is not mainly the API bill. That…

4 days ago

How to Use the WP Theme Customizer API for Dynamic Sites

⚡ TL;DR The WordPress theme customizer API is still a very practical tool for classic…

5 days ago

Automating Programmatic SEO: 100 Pages/Day with OpenAI & WordPress (2026)

The short answer: yes, you can automate the creation and publishing of hundreds of SEO…

6 days ago

I Built a Content Brief Agent in n8n That Produces Briefs I Actually Use — Full Workflow

Most AI-generated content briefs are useless for the same reason: they're built from nothing but…

7 days ago