Workflow infrastructure

Real infrastructure under every workflow.

QuickFlo is a platform for building business logic as workflows, with a visual builder or an agent, and running it on whatever starts the work: webhooks, schedules, form submissions, events. It is for the parts of your business that need to stay flexible, dynamic, and config-driven, and for the projects that orchestrate across many systems. It was born from the things we wished we had while delivering software to customers. We did not take any of them lightly.

Scale

It does not fall over at 50k records.

Most steps never hold the whole dataset in memory. Work streams through in chunks and spills to object storage when it outgrows its budget, loading a chunk at a time. The few steps that truly need everything at once can still ask for it, but on the common path a run over 50k records stays about as light as a run over fifty.

And every execution is its own durable job on a queue. It survives restarts, and if a worker dies mid-run the job is redelivered, not lost. Each run lands on a worker with a fixed memory budget, so one heavy or runaway execution fails inside its own budget instead of crowding out its neighbors. You scale by running more of these isolated jobs, not by hoping a single process can hold everything.

run / import-and-scrub · 0xb8e2completed
50,000records processed
  • memory budget100 MB
  • peak held64 MB
  • streamed inchunks of 5,000
  • spilledto object storage
one durable job, one bounded worker · streamed in chunks and spilled to storage · peak memory stayed under budget
Failure you can reason about

Know what failed, and what is safe to retry.

QuickFlo separates two kinds of failure. An execution error means a step threw. An operational error means the step ran but the outcome signals a problem, like an HTTP request that comes back 400. We treat both as real failures, so by default the run stops, because a 400 is not a success. On many platforms that call "completed", so the run looks green while data quietly went nowhere. Both kinds land, classified, in an $errors view, and when you do want a run to push past them you turn that on per step or per workflow.

run / nightly-sync · 0x4c0d · $errors
  • execution · haltstransform-addressTypeErrorcannot read "zip" of null · record 8,402 has no address object
  • operational · haltscrm-upsertHTTP_CLIENT_ERRORupstream returned 400: contact email already exists for this account
both errors halted the run by default · continue-on-error is opt-in, per step or workflow
The template system

One template syntax, down to the conditions.

Steps are configured with {{ }} expressions, and a variable picker that already knows the shape of the data flowing into each step. There are filters for the common transforms and predictable output shapes you can rely on downstream. The same syntax works in every field, including the if and switch conditions other tools make you write in a second, clumsier language.

step config · enrich-contact
Full name
{{$item.name|strip}}
Current item
  • namestring
  • emailstring
  • phonestring
the same {{ }} syntax in every field, including if and switch conditions
The code step

When you need code, you need real code.

QuickFlo's core.code step runs on Deno. It is sandboxed by default, with an explicit permission model for the network and the filesystem, and it can import nearly any package at runtime, straight from npm or a URL. Most code steps are a locked sandbox with a fixed standard library. An escape hatch that cannot import the library you need is not an escape hatch.

step config · core.code · parse-upload

import { parse } from "npm:csv-parse/sync"

// $input.csv arrives from the previous step

const rows = parse($input.csv, {

columns: true,

skip_empty_lines: true,

})

return { count: rows.length, rows }

permissions: net=off, read=./tmp · imports resolved at runtime from npm
Steps and connectors

Connector count is a vanity metric.

QuickFlo ships around eighty native steps, each one deeply built, and connects to anything with an API. We ship fewer native steps on purpose: instead of chasing a five-thousand connector roadmap, the packaging system lets you, or a partner, build the missing piece once and install it everywhere. You never wait on our roadmap.

step catalog · ~80 native · any API · package your own
  • nativehttp.requestcall any API
  • nativedata.filterstreamed transform
  • nativecore.codeDeno, any package
  • packagedacme.crm.syncbuilt once, installed everywhere
the roadmap you wait on vs the package you ship yourself
Data and dashboards

Reporting is not a second product.

Data stores are queryable tables you build as part of the solution, and dashboards read straight from them. The automation already holds the data, so the reporting ships with it. No external warehouse, no separate BI tool.

data store · leads · dashboard / pipeline
companystagevalue
Northwindqualified$24,000
Initechproposal$18,500
Hooliwon$61,200
  • $142kopen pipeline
  • $61kwon this month
  • 318active leads
dashboard reads straight from the data store · no external warehouse
Packaging

Copying the workflow was never the hard part.

Most tools let you template a workflow and little else. The data model, the dashboards, the triggers, and the settings get rebuilt by hand on every new project, and that handwork is where the time goes. QuickFlo packages all of it into one versioned unit, so the next customer is an install with their own settings, not a rebuild of everything the workflow needs around it.

package · @acme/contact-center · v1.4.0
  • ✓ shipsWorkflows6, versioned together
  • ✓ shipsTriggersusually rebuilt by hand
  • ✓ shipsData modelusually rebuilt by hand
  • ✓ shipsDashboardsusually rebuilt by hand
  • ✓ shipsConnectorsthe custom CRM step, included
  • ✓ shipsSettings and defaultsusually rebuilt by hand
the surfaces other tools make you rebuild by hand ship in the package too
Agents on the same engine

Agents are just steps. Traced ones.

An agent in QuickFlo is a workflow step that calls your other workflows as tools, and it runs on the same engine with the same controls as everything else. Every tool it reaches for is written to the run trace: the tool name, the arguments it was given, the result it returned. The same record you would read to debug a webhook is the record you read to audit an agent.

run / agent-turn · 0xa3f1completed
agentReconcile the four overdue accounts and notify their owners.
  • workflows.accounts.find-overdue210msdays_overdue >= 30, limit: 44 accounts returned
  • workflows.accounts.load-owners146msaccount_ids: [a1, a2, a3, a4]3 owners resolved
  • workflows.billing.compute-balance88msaccount_ids: [a1, a2, a3, a4]total $48,210 outstanding
  • workflows.salesforce.updategatedstatus: "In Review", 4 recordsheld: requires approval
  • workflows.slack.dm-owner124msto: 3 owners, template: overdue-review3 messages queued
  • workflows.audit.record41msevent: overdue-review-runwritten
6 tool calls · 1 held for approval · full trace retained on the run
Guardrails

Writes wait for a human.

Any tool can be marked as requiring approval. When the agent tries to call one, execution stops at the gate and the person in the chat is asked, in plain terms, to approve or deny the exact action and its arguments. Reads can flow freely. The things that change records do not run until someone allows them.

embedded agent · support-desk
Close the four resolved tickets from this morning and update the accounts.
I will close those tickets and sync the account records. One step needs your approval before it runs.
Approval required
salesforce.update4 records
Sets status to Closed on accounts owned by 3 reps.
ApproveDeny
Held at the gate. The write does not execute until approved.
The builder

It builds from a prompt, and reads its own failures.

Describing a workflow and getting one back is the easy half. The half that matters is the other direction: point the builder at a failed run and it reads the trace, tells you which step broke and why, and proposes the change. Building and debugging are the same conversation.

builder · execution 0x71c2 (failed)
step dedupe failedmissing key "email": records from import.csv use "Email Address"
The dedupe step keys on email, but the uploaded file labels that column Email Address. Add a normalize step before dedupe to map the header, then re-run.
proposed change+ insert normalize before dedupe+ map "Email Address"emailApply and re-run
Get started

See the trace for yourself.

Tell us what you want an agent to do in your stack. We will show you how it runs, how it is traced, and where the gates sit.

Install the CLI