Describe, Deploy, Own

Build automations using natural language or our visual canvas. We generate raw TypeScript and deploy it directly to your Cloudflare account.

drag to spin
How it works

From canvas to cloud in minutes.

Describe it, or draw it. Attach Credentials. Deploy.

nodes2cloud / flows / refund-approval
WEWebhooktrigger
POST /dispute
OCOpenAIai
gpt-4o-mini
SPSlackmessaging
#billing
WFWait for Eventflow
timeout 24 hours
ICIf/Conditionflow
json.approval.approved
HRHTTP Requestcore
POST api.stripe.com
graph → IR → worker
src/index.tsyours to export
// Generated by N2C Compiler
// Workflow: "Refund approval" (ID: "refund_approval")
// Target: Cloudflare Workflow

import { WorkflowEntrypoint } from "cloudflare:workers";

export class N2CWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    const env = this.env;
    const ctx = {
      input: event.payload,
      output: {},
      nodes: {},
      env,
      execution: {
        id: crypto.randomUUID(),
        startedAt: new Date().toISOString(),
        durationMs: 0,
        retryCount: 0,
        currentStep: "",
        debugMode: false,
        traceId: crypto.randomUUID(),
        logger: { log: (msg) => console.log(msg), error: (msg) => console.error(msg) },
      },
      workflow: {
        id: "refund_approval",
        name: "Refund approval",
        version: 1,
        triggerType: "webhook",
        settings: {},
      },
      node: {},
    };

    // === Node: hook (webhook_trigger) ===
    ctx.nodes["hook"] = ctx.input;
    // === Node: summarize (openai) ===
    ctx.nodes["summarize"] = await step.do("summarize", async () => {
      const summarize_resRes = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: {
          "Authorization": "Bearer " + env.CREDENTIAL_SUMMARIZE,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: `One line: why is ${ctx.input?.body?.customer} disputing?` }] }),
      });
      if (!summarize_resRes.ok) {
        throw new Error("OpenAI API error " + summarize_resRes.status + ": " + (await summarize_resRes.text()));
      }
      const summarize_res = await summarize_resRes.json();
      return summarize_res;
    });
    // === Node: ask (slack) ===
    ctx.nodes["ask"] = await step.do("ask", async () => {
      const ask_resRes = await fetch("https://slack.com/api/chat.postMessage", {
        method: "POST",
        headers: {
          "Authorization": "Bearer " + env.CREDENTIAL_ASK,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ channel: "#billing", text: `Approve refund? ${ctx.input?.summarize?.text}` }),
      });
      if (!ask_resRes.ok) {
        throw new Error("Slack API error " + ask_resRes.status + ": " + (await ask_resRes.text()));
      }
      const ask_res = await ask_resRes.json();
      return ask_res;
    });
    // === Node: approval (wait_for_event) ===
    ctx.nodes["approval"] = { ...(await step.waitForEvent("approval", { type: "refund_decision", timeout: "24 hours" })), timedOut: false };
    // === Node: gate (if) ===
    if (ctx.input?.approval?.approved) {
      // === Node: refund (http_request) ===
      ctx.nodes["refund"] = await step.do("refund", async () => {
        const refund_res = await fetch("https://api.stripe.com/v1/refunds", { method: "POST", signal: AbortSignal.timeout(30000) });
        return { status: refund_res.status, body: await refund_res.text() };
      });
    } else {
    }
  }
}

export default {
  async fetch(request, env, executionCtx) {
    if (["/favicon.ico","/robots.txt","/apple-touch-icon.png"].includes(new URL(request.url).pathname)) {
      return new Response(null, { status: 404 });
    }
    if (request.method === "POST" && new URL(request.url).pathname === "/__resume/approval") {
      {
        const legacyProvided = request.headers.get("X-CC-Webhook-Secret") ?? "";
        const expected = env.CC_RESUME_SECRET_APPROVAL ?? "";
        const legacyMaxLen = Math.max(legacyProvided.length, expected.length);
        let legacyMismatch = legacyProvided.length === expected.length ? 0 : 1;
        for (let i = 0; i < legacyMaxLen; i++) {
          legacyMismatch |= (legacyProvided.charCodeAt(i) || 0) ^ (expected.charCodeAt(i) || 0);
        }
        let authOk = legacyMismatch === 0 && expected !== "";

        const signatureHeader = request.headers.get("X-CC-Signature");
        if (!authOk && signatureHeader && expected !== "") {
          const sigMatch = /^t=(\d+),v1=([0-9a-f]+)$/.exec(signatureHeader);
          if (sigMatch) {
            const signedTimestamp = Number(sigMatch[1]);
            const ageSeconds = Math.abs(Date.now() / 1000 - signedTimestamp);
            if (Number.isFinite(ageSeconds) && ageSeconds <= 300) {
              const rawBody = await request.clone().text();
              const hmacKey = await crypto.subtle.importKey("raw", new TextEncoder().encode(expected), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
              const signatureBytes = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(`${signedTimestamp}.${rawBody}`));
              const computedSignature = [...new Uint8Array(signatureBytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
              const providedSignature = sigMatch[2];
              const sigMaxLen = Math.max(providedSignature.length, computedSignature.length);
              let sigMismatch = providedSignature.length === computedSignature.length ? 0 : 1;
              for (let i = 0; i < sigMaxLen; i++) {
                sigMismatch |= (providedSignature.charCodeAt(i) || 0) ^ (computedSignature.charCodeAt(i) || 0);
              }
              authOk = sigMismatch === 0;
            }
          }
        }

        if (!authOk) {
          return new Response("Unauthorized", { status: 401 });
        }
      }
      const resumeBody = await request.json().catch(() => ({}));
      const instanceId = typeof resumeBody.instanceId === "string" ? resumeBody.instanceId : "";
      if (instanceId === "") {
        return Response.json({ error: "instanceId is required" }, { status: 400 });
      }
      let instance;
      try {
        instance = await env.WORKFLOW.get(instanceId);
      } catch {
        return Response.json({ error: "Unknown workflow instance" }, { status: 404 });
      }
      try {
        await instance.sendEvent({ type: "refund_decision", payload: resumeBody.payload });
      } catch (resumeErr) {
        return Response.json({ error: String((resumeErr && resumeErr.message) || resumeErr) }, { status: 409 });
      }
      return Response.json({ ok: true, instanceId, type: "refund_decision" });
    }
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }
    {
      const legacyProvided = request.headers.get("X-CC-Webhook-Secret") ?? "";
      const expected = env.CC_WEBHOOK_SECRET_HOOK ?? "";
      const legacyMaxLen = Math.max(legacyProvided.length, expected.length);
      let legacyMismatch = legacyProvided.length === expected.length ? 0 : 1;
      for (let i = 0; i < legacyMaxLen; i++) {
        legacyMismatch |= (legacyProvided.charCodeAt(i) || 0) ^ (expected.charCodeAt(i) || 0);
      }
      let authOk = legacyMismatch === 0 && expected !== "";

      const signatureHeader = request.headers.get("X-CC-Signature");
      if (!authOk && signatureHeader && expected !== "") {
        const sigMatch = /^t=(\d+),v1=([0-9a-f]+)$/.exec(signatureHeader);
        if (sigMatch) {
          const signedTimestamp = Number(sigMatch[1]);
          const ageSeconds = Math.abs(Date.now() / 1000 - signedTimestamp);
          if (Number.isFinite(ageSeconds) && ageSeconds <= 300) {
            const rawBody = await request.clone().text();
            const hmacKey = await crypto.subtle.importKey("raw", new TextEncoder().encode(expected), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
            const signatureBytes = await crypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(`${signedTimestamp}.${rawBody}`));
            const computedSignature = [...new Uint8Array(signatureBytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
            const providedSignature = sigMatch[2];
            const sigMaxLen = Math.max(providedSignature.length, computedSignature.length);
            let sigMismatch = providedSignature.length === computedSignature.length ? 0 : 1;
            for (let i = 0; i < sigMaxLen; i++) {
              sigMismatch |= (providedSignature.charCodeAt(i) || 0) ^ (computedSignature.charCodeAt(i) || 0);
            }
            authOk = sigMismatch === 0;
          }
        }
      }

      if (!authOk) {
        return new Response("Unauthorized", { status: 401 });
      }
    }
    const payload = await request.json().catch(() => ({}));
    const instance = await env.WORKFLOW.create({ params: payload });
    return Response.json({ instanceId: instance.id, status: "started" });
  },
};
Deploymentlive in your account
IADLHRFRACDGAMSSINNRTSYDGRUJNBBOMYYZ

Cloudflare runs 330+ cities. You pick none of them.

Compiles to
Cloudflare Workflow
Bundle
8.3 KB gzipped
Worker secrets
4 injected at deploy
Per-run fee to us
$0.00
Integrations

Yes, it connects to that.

Connect to 100+ tools out of the box, or use native HTTP nodes to bypass our roadmap and connect to anything else.

SlackOpenAIStripeGitHubNotionAirtableHubSpotSupabaseGoogle SheetsAnthropicAnd more

Pre-wired for the modern stack.

What you build

Infrastructure-grade workflows.

Our analyzer reads your graph's shape and automatically assigns the correct Cloudflare compile target under the hood.

Human in the loop

Refunds a human has to approve

compiles to Cloudflare Workflow
AI agent

A support agent that files its own tickets

compiles to Email Worker

The 9am revenue digest

compiles to Scheduled Worker
RAG

Ask your docs, in chat

compiles to Worker

Form to CRM, without the copy-paste

compiles to Worker

Nightly sync into the warehouse

compiles to Cloudflare Workflow
How we do it

Fast. Light. your edge.

We deploy raw V8 isolates directly to your Cloudflare account. Workflows are distributed globally via Anycast. Executed with 0ms cold starts and zero idle costs.

A userclient
anywhere
Nearest data centreanycast
chosen by the network
Your isolateworker
cold-started in 0 ms
Responseoutbound
never left the edge
Already running atIADLHRFRACDGAMSSINNRTSYDGRUJNBBOMYYZ
Runtime
V8 isolates
Cold start
0 ms
Idle cost
$0.00
Region to pick
none
Why it's different

We don't charge per run, because we don't run it.

Everyone else sells you a canvas and rents you their interpreter. This difference is not a pricing decision we could reverse next quarter — it falls out of where the code executes.

It runs in your account

The Worker executes in your Cloudflare tenant, billed by Cloudflare to you. Your payloads never reach our servers.

tenant: yoursbilled_by: cloudflare

One disclosed exception: while a live debug session is open in the editor, your Worker streams its console output through us so we can show it to you. Nothing from that stream is written to disk or to a database, and it ends when you close the panel.

Credentials, sealed

Encrypted in our custody with AES-GCM, decrypted only at deploy, and injected straight into your Worker's secrets.

AES-GCM-256 · HKDF-SHA256

Zapier · Make · Workato

  • Metered per run — the bill grows with your traffic
  • Your payloads flow through their servers
  • Delete the account and every workflow stops
  • Cost scales with volume, forever

Nodes2Cloud

  • 100,000 requests a day free. $5/mo covers 10 million.
  • Payloads never reach us — the Worker runs in your account
  • Delete the account and every workflow keeps running
  • $18/mo flat, whatever your volume does

Leave whenever

Export the TypeScript and the wrangler config any time. Delete us tomorrow and every deployed workflow keeps running.

src/index.ts · wrangler.jsonc
Our cut of every run

$0.00

Forever. We don't host the runtime, so there is no meter to run.

Cost

Build Once. The rest is Cloudflare's, on your bill.

Our figure never moves. Cloudflare's grows with the steps your workflow actually spends, at their published rate — and the total still lands two orders of magnitude under a per-task platform at any volume worth the name.

Monthly cost · both axes logarithmic250,000 runs
YouA leading per-task platformHover or drag the chart

Edge compute reduces cost significantly

Against a per-task platform

86×

less, at this volume — $2,330 (est.) a month against $27.

Nodes2Cloud Pro
$18flat and just once
Cloudflare requests
$5their bill, your account
Cloudflare steps · 4/run
$4not yet billed
Pricing

Flat, and unrelated to volume.

You pay us for the builder. You pay Cloudflare for the compute, at their published rate, on your own bill.

Developer

Free forever

$0

For side projects and trying the canvas out.

  • Up to 3 workflows
  • Runs on Cloudflare's free tier
  • Full project export, any time
  • All 162 nodes
Start building

Pro

Most popular

$18

per month, or $180 a year

For production workflows you depend on.

  • Unlimited workflows
  • 1-click n8n import
  • Encrypted credential management
  • 500 AI credits and MCP support
  • GitHub auto-push and drift detection
Go Pro

Team

Waitlist

Let's talk

For one contract, one invoice, one person to call.

  • Everything in Pro
  • Shared credentials
  • Real-time multiplayer editing
  • Audit log
Join the waitlist
FAQ

The questions you were going to ask anyway.

Including the two we would rather you did not, which is why they are here.

You need a Cloudflare account — that part is not optional, because the Worker runs in it. You do not need to know Workers. You connect the account once, and we create the databases, buckets and queues each workflow needs and push your API keys in as secrets. If you have never opened a terminal, this still works; if you have, you will recognise everything it made.

No. Cloudflare's free plan covers 100,000 Worker requests a day, D1 databases and Workflows, and the editor tells you how many runs a day your specific graph affords on it before you deploy. You move to a paid Cloudflare plan when your volume says so, and you pay Cloudflare directly — we never sit between you and that bill.

162 nodes ship today, 104of them talking to a third-party API, and the wall further up this page names every company we talk to rather than rounding it into a number — look for yours before you sign up. If it is not there, the HTTP Request node authenticates against any REST API and the Code node runs your own JavaScript, so a missing node is a bit more typing rather than a wall.

There is an importer in the editor for 108node types, and it gives you a per-node coverage report before anything is created. Anything it cannot map arrives as an explicit “unsupported” placeholder rather than quietly swapped for something that looks similar, so the gaps are yours to decide about. Credentials are dropped on the way in by design — we read their names, never their values — so you reconnect each account once here.

The Worker runs in your account, so the payloads it processes do not pass through us. Your API keys are encrypted under a key derived per user, with the master key held in Cloudflare's secret store rather than our database.

One disclosed exception: when you open the live debug panel in the editor, your Worker streams its per-node console output through one of our Workers so we can display it. Nothing from that stream is written to a database or to disk, it only exists while the panel is open, and it is a paid-plan feature you switch on deliberately. We would rather write that down than let you find it in a network tab.

Your deployed Workers keep running, because they are in your account and there is nothing of ours in them. What you lose is the editor. Export the project first and you have a normal repo — source, config, README, and the graph as JSON — that deploys from your own CI. Windmill's git-backed export is genuinely better than ours; ours is a compiled artifact rather than round-trippable source files.

Worth knowing before you commit: schedules run in UTC (the timezone picker is captured but does not do anything yet), there is no marketplace, and .xlsx and .zip file handling is deferred. We publish no uptime SLA and hold no compliance certification — if you need one contractually, we are not the right choice yet, and we would rather say so here than in a procurement call.

Not today, and we are not going to imply otherwise. Releasing the compiler, node library and shared packages under Apache-2.0 is a plan with real prerequisites still outstanding, not a thing you can go and clone. When it ships we will say so loudly; until then it is not a reason to choose us.

Take back ownership of your automations.

Your first workflow runs in your Cloudflare account, on your bill — and keeps running whatever happens to us.

No credit card · export the project whenever you like