Skip to main content
Build a pipeline that ingests form data, persists it in Postgres, and emails a digest to your team. Runs are triggered by an inbound webhook; a separate scheduled trigger (external cron → same workflow or a digest-only variant) can send the daily email.

What you’ll build

Outcome: Every submission is stored with a timestamp. Once per day, stakeholders receive an HTML email listing new leads or tickets.

Prerequisites

  • project_contributor in your workspace
  • Postgres connection with INSERT and SELECT on a form_submissions table
  • Resend connection with a verified sending domain
  • An LLM provider configured in Providers (for the digest narrative)
  • Inbound webhook feature enabled for your environment

Create the table

Run once against your database:
CREATE TABLE IF NOT EXISTS form_submissions (
  id SERIAL PRIMARY KEY,
  email TEXT NOT NULL,
  name TEXT,
  message TEXT,
  source TEXT DEFAULT 'webhook',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Connectors to install

AdapterPurpose
postgresStore and query submissions
resendSend digest email

Part 1 — Ingest submissions

1

Create the ingest workflow

In Workflow Studio, create a workflow named form-submission-ingest.
2

Add validate step

Add a lua_script step that requires email and normalizes input from the webhook body.
3

Add Postgres insert

Add mcp_callpostgres_exec on your postgres instance to INSERT a row.
4

Create webhook subscription

Link an inbound webhook subscription to this workflow. Configure your form tool (Typeform, Webflow, custom app) to POST JSON to the subscription URL.
5

Test

Submit a test form. Confirm a row appears in form_submissions and the run succeeds in Command Center.

Validate step

{
  "id": "validate",
  "type": "lua_script",
  "name": "Validate submission",
  "script": "if not input.email or input.email == '' then error('email required') end\nreturn {\n  email = input.email,\n  name = input.name or '',\n  message = input.message or '',\n  source = input.source or 'webhook'\n}",
  "timeout_s": 10
}

Insert step

{
  "id": "insert-row",
  "type": "mcp_call",
  "name": "Save submission",
  "tool_name": "postgres_exec",
  "tool_args": {
    "sql": "INSERT INTO form_submissions (email, name, message, source) VALUES ($1, $2, $3, $4)",
    "args": [
      "{{steps.validate.result.email}}",
      "{{steps.validate.result.name}}",
      "{{steps.validate.result.message}}",
      "{{steps.validate.result.source}}"
    ]
  },
  "depends_on": ["validate"],
  "timeout_s": 30
}

Webhook payload example

Your form should POST fields that map to workflow input:
{
  "email": "alex@example.com",
  "name": "Alex Kim",
  "message": "Interested in enterprise plan",
  "source": "landing-page"
}

Part 2 — Daily digest email

Create a second workflow form-daily-digest (or branch on input.digest in one graph).
1

Query last 24 hours

mcp_callpostgres_query for rows where created_at > NOW() - INTERVAL '1 day'.
2

Summarize with LLM

llm_call with a prompt that includes {{steps.query-submissions.result.rows}} and asks for a short HTML bullet list.
3

Send email

mcp_callsend_email on your resend instance with html from the LLM step.
4

Schedule externally

Use GitHub Actions, cron, or your cloud scheduler to POST to the digest webhook URL once per day (see Workflow patterns).

Query step

{
  "id": "query-submissions",
  "type": "mcp_call",
  "name": "Fetch today's submissions",
  "tool_name": "postgres_query",
  "tool_args": {
    "sql": "SELECT email, name, message, source, created_at FROM form_submissions WHERE created_at >= NOW() - INTERVAL '1 day' ORDER BY created_at DESC"
  },
  "timeout_s": 30
}

LLM step

{
  "id": "summarize",
  "type": "llm_call",
  "name": "Write digest narrative",
  "model": "gpt-4o-mini",
  "prompt": "You are an ops assistant. Given these form submissions as JSON, write a concise HTML email body with a bullet per submission (email, name, message). Submissions: {{steps.query-submissions.result.rows}}",
  "depends_on": ["query-submissions"],
  "timeout_s": 120
}

Send step

{
  "id": "send-digest",
  "type": "mcp_call",
  "name": "Email team digest",
  "tool_name": "send_email",
  "tool_args": {
    "to": "team@example.com",
    "subject": "Daily form submissions — {{input.report_date}}",
    "html": "{{steps.summarize.result.text}}"
  },
  "depends_on": ["summarize"],
  "timeout_s": 30
}

Full workflow graphs (copy-paste)

Bind your postgres and resend MCP instances on each mcp_call step in Workflow Studio.

Ingest workflow (form-submission-ingest)

{
  "tenant_id": "your-workspace-slug",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440020",
  "params": {},
  "steps": [
    {
      "id": "validate",
      "type": "lua_script",
      "name": "Validate submission",
      "script": "if not input.email or input.email == '' then error('email required') end\nreturn {\n  email = input.email,\n  name = input.name or '',\n  message = input.message or '',\n  source = input.source or 'webhook'\n}",
      "timeout_s": 10
    },
    {
      "id": "insert-row",
      "type": "mcp_call",
      "name": "Save submission",
      "tool_name": "postgres_exec",
      "tool_args": {
        "sql": "INSERT INTO form_submissions (email, name, message, source) VALUES ($1, $2, $3, $4)",
        "args": [
          "{{steps.validate.result.email}}",
          "{{steps.validate.result.name}}",
          "{{steps.validate.result.message}}",
          "{{steps.validate.result.source}}"
        ]
      },
      "depends_on": ["validate"],
      "timeout_s": 30
    }
  ]
}

Digest workflow (form-daily-digest)

{
  "tenant_id": "your-workspace-slug",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440021",
  "params": {},
  "steps": [
    {
      "id": "query-submissions",
      "type": "mcp_call",
      "name": "Fetch today's submissions",
      "tool_name": "postgres_query",
      "tool_args": {
        "sql": "SELECT email, name, message, source, created_at FROM form_submissions WHERE created_at >= NOW() - INTERVAL '1 day' ORDER BY created_at DESC"
      },
      "timeout_s": 30
    },
    {
      "id": "summarize",
      "type": "llm_call",
      "name": "Write digest narrative",
      "model": "gpt-4o-mini",
      "prompt": "You are an ops assistant. Given these form submissions as JSON, write a concise HTML email body with a bullet per submission (email, name, message). Submissions: {{steps.query-submissions.result.rows}}",
      "depends_on": ["query-submissions"],
      "timeout_s": 120
    },
    {
      "id": "send-digest",
      "type": "mcp_call",
      "name": "Email team digest",
      "tool_name": "send_email",
      "tool_args": {
        "to": "team@example.com",
        "subject": "Daily form submissions — {{input.report_date}}",
        "html": "{{steps.summarize.result.text}}"
      },
      "depends_on": ["summarize"],
      "timeout_s": 30
    }
  ]
}
Wire the ingest workflow to your form webhook URL. Schedule the digest workflow with an external cron POST — see Inbound webhooks.

Idempotency

Form providers may retry webhooks. Options:
  • Add a unique submission_id column and use ON CONFLICT DO NOTHING
  • Check for duplicate email + timestamp in a Lua step before INSERT

Variations

  • Swap Resend for Gmail if you prefer sending from a Google mailbox.
  • Append rows to Google Sheets instead of Postgres for a no-DB setup.
  • Add human_task approval before the digest sends on Mondays only.