Getting started with Flowbrew
Flowbrew turns plain-language workflow descriptions into versioned Cloudflare Workflows and lets those workflows call plugin-provided actions. This page is a short map for someone operating Flowbrew through MCP and then moving on to plugin development.
For exact tool names and inputs, see the MCP reference.
Concept map
Section titled “Concept map”Workspace
Section titled “Workspace”A workspace is the top-level authorization and isolation boundary. An API key or OAuth identity resolves to one workspace, and MCP operations derive that workspace from the credential rather than accepting a caller-supplied workspace ID.
Project (optional)
Section titled “Project (optional)”A project is an organizational scope inside a workspace. A workflow may belong to one project or remain workspace-global; deleting a project is blocked while workflows still reference it. Project-scoped connections and triggers can only be used with workflows in the same project, while workspace-scoped resources can be used across the workspace where the operation permits it.
Workflow
Section titled “Workflow”A workflow is created first as a metadata-only shell with a workspace-unique name and an optional project. Each successful description-driven update compiles and publishes a new version, and new instances start from the current version while existing instances retain the version on which they began.
Plugin
Section titled “Plugin”A plugin is a versioned manifest containing bricks, connection types, and trigger definitions. Public plugins are visible to every workspace; private plugins are owned by and installed into the workspace that uploads them.
Authors can test a candidate in one workspace before making it current. See plugin version channels and CI recipe.
A brick is a plugin-provided action with an HTTPS endpoint plus input and output
schemas. A brick can declare named connection slots, and generated workflow
code invokes it by the stable <plugin-slug>.<brick-slug> ID.
Connection type and connection
Section titled “Connection type and connection”A connection type belongs to a plugin version and defines a credential schema, optional fixed plugin configuration, and a same-plugin validation brick. A connection is a user’s encrypted credential instance of that type, scoped to the workspace or to one project; credential creation and rotation happen in the web console.
This mechanism is not limited to built-in integrations. Any plugin — public or
private — can declare its own connection types and reference them from brick
connection slots, so a private plugin’s own fetch() calls to an internal
service can use an encrypted, rotatable connection instead of an env-var
secret. See the SDK README’s issue-tools / Jira example linked under
Build a plugin for a complete custom-plugin walkthrough.
Trigger
Section titled “Trigger”A plugin manifest declares trigger definitions with lifecycle endpoints, configuration and event schemas, and optionally one required connection type. An MCP-created trigger instance is workspace- or project-scoped, and a trigger subscription connects that instance to a workflow; each instance accepts at most five subscriptions.
Set up access
Section titled “Set up access”- Sign in to the web console at
https://console.flowbrew.app(or your deployment’s console URL) and create or select a workspace. - Create a workspace API key in API Keys. The raw key is shown only once.
- Point an MCP client at
https://mcp.flowbrew.app/mcp(or your deployment’s own/mcpendpoint) and authenticate with that key as a bearer token. A deployment with the OAuth bridge enabled can authenticate an MCP client through OAuth instead.
For local or scripted calls from a repository checkout, the repository helper reads the local SST output
by default and accepts TASKFLOW_MCP_URL for your Flowbrew MCP endpoint and
TASKFLOW_API_KEY for your Flowbrew workspace API key:
TASKFLOW_MCP_URL='https://mcp.flowbrew.app/mcp' \TASKFLOW_API_KEY='<your-flowbrew-api-key>' bun run mcp:call list_projects '{}'The current product boundary is:
- The web console creates and manages workspaces/organizations, membership and invitations, and raw API keys. It also creates, rotates, revokes, and purges credential-bearing connections.
- MCP owns direct synchronous brick execution, workflow and trigger-instance/subscription lifecycle, and project lifecycle. It can list or revoke existing API keys.
- MCP can list connection metadata and create a credential-less pending connection with
create_connection, but cannot create or rotate credentials. The console does not currently expose workflow or trigger lifecycle routes.
That boundary is slightly less absolute than “all API-key management is in the
console”: list_api_keys and revoke_api_key are live MCP tools, but creating a
key (and receiving its one-time raw value) remains a console operation.
Keep workflows thin
Section titled “Keep workflows thin”Workflows are thin orchestration/glue: call bricks, branch, and pass small
results between calls. Transformation-heavy logic — HTML/text parsing,
multi-step extraction, or formatting large payloads — belongs in a custom
brick, not in generated workflow code or the description handed to
update_workflow.
Build that brick with @flowbrew/plugin-sdk. Its handler can use normal
server-side fetch, parse, and format without the workflow’s outbound network
restriction or a generic brick’s input/output schema constraints. Return a
small result; the workflow then calls that brick and passes the result to the
next brick. See the SDK quickstart in the plugin-building section below.
Rule of thumb before calling update_workflow: if a step needs more than
simple field mapping on a brick’s output, make it a brick.
Call a brick directly through MCP
Section titled “Call a brick directly through MCP”Use call_brick for one synchronous action without a workflow shell. Inspect
list_plugins and list_connections({ projectId: null }), then call:
{ "brickId": "issue-tools.create-issue", "input": { "title": "Investigate preview failure" }, "connections": { "primary": "<configured-workspace-connection-id>" }}input is required and accepts any JSON value allowed by the brick schema.
Bindings are explicit: connectionId shorthand works only for exactly one
slot and cannot accompany connections. For zero/multiple slots, omit shorthand
and use a map for any bindings. There is no automatic selection or project authority. Eligible connections
must belong to this workspace, be workspace-scoped, configured, and match the
slot’s provider/type; revision is rechecked during redemption.
Success returns { callId, brickId, pluginVersionId, output }. One call makes one
attempt; repeated calls are new executions. Timeout/disconnect means effects may
have occurred. Execution errors report not_started or unknown; output
validation failure cannot undo effects. input.dryRun is ordinary business input,
passed through unchanged, with no platform dry-run promise. Limits are 30 seconds
overall, at most 25 seconds and the manifest timeout at the endpoint, 1 MiB UTF-8
input/response and 10 admissions per 60 seconds per workspace (coarse, per location).
Callbacks, child workflow starts and project-required core bricks are unsupported.
Direct records retain bounded business payloads for a 30-day working window,
with cleanup that can lag, and are not a guaranteed audit or replay log. There
is no direct-call history reader in v1. See the
complete contract.
Build and run a workflow through MCP
Section titled “Build and run a workflow through MCP”The normal order is shell, deployment, start, then status polling.
1. Inspect the available scope
Section titled “1. Inspect the available scope”Use list_projects if the workflow should be project-scoped. list_plugins
shows visible plugins and their connection types, brick contracts, and trigger
definitions. list_connections returns metadata for connections usable at
workspace scope or in a selected project.
list_connections requires an explicit projectId: pass null for only
workspace-scoped connections, or a project ID for workspace connections plus
connections belonging to that project.
2. Create an empty workflow shell
Section titled “2. Create an empty workflow shell”Call create_workflow with a name and, optionally, projectId. The result has
currentVersionName: null and currentVersionNumber: null; it cannot be
started until code is deployed.
3. Generate and deploy workflow code
Section titled “3. Generate and deploy workflow code”Call update_workflow with the shell’s id and a plain-language
description. Flowbrew supplies the code generator with the visible brick
catalog and the configured connections allowed for the workflow’s scope.
Generation produces a complete TypeScript file, not a patch. The candidate is compiled, then executed against independently generated declarative cases in an isolated mock runner. Every case must pass before storage/publication. No real Workflow instance or brick call is created. Generation, compilation, malformed or empty suites, case failures, infrastructure errors and timeouts preserve the live version. Tool errors provide numbered pass/fail diagnostics with fixed codes; refine the description and retry. Automatic repair is not performed.
Name and project changes use the same tool in a separate metadata-only call.
Do not combine description with name or projectId.
4. Start one workflow instance
Section titled “4. Start one workflow instance”Call start_workflow with either workflowId or name, a non-empty
idempotencyKey, and an optional JSON payload. Caller payload becomes
event.payload.input inside the workflow.
The idempotency key is workspace-scoped. Retrying with the same key returns the same instance rather than intentionally starting another one, so use a new key for each new logical run.
start_workflow returns an instanceId immediately; it does not wait for the
workflow to finish.
5. Poll for the result
Section titled “5. Poll for the result”Pass the instanceId to read_instance. One call can long-poll for about 50
seconds and returns the current status, plus output or error when available.
Use list_instances to rediscover recent instances or filter them by workflow.
6. Add a brick with a connection binding
Section titled “6. Add a brick with a connection binding”There is no separate “add brick” MCP mutation. Create and validate the
connection in the web console, use list_connections to obtain its ID, then
describe the desired plugin action in a description-driven update_workflow.
Flowbrew generates the brick call and binds required named slots to literal
connection IDs; compilation rejects missing, unknown, wrongly typed, or
out-of-scope bindings.
Only configured workspace connections and configured connections belonging
to the workflow’s project are offered to workflow generation. A
workspace-global workflow cannot use a project-owned connection.
7. Register and subscribe a trigger
Section titled “7. Register and subscribe a trigger”Choose a trigger definition from list_plugins, then call
create_trigger_instance with its id as triggerDefinitionId and:
scope: "workspace"andprojectId: null, orscope: "project"and the matching project ID;- configuration that matches the definition’s schema; and
- either the required compatible
connectionIdornullfor a connectionless definition.
Creation calls the plugin’s registration endpoint and becomes active only
after the plugin acknowledges it. Then call create_trigger_subscription with
the returned trigger-instance ID and the workflow ID. A project-scoped trigger
can subscribe only workflows in that project; a workspace-scoped trigger can
subscribe workflows anywhere in its workspace.
Call list_trigger_subscriptions, optionally filtered by triggerInstanceId
and/or workflowId, to find an existing subscription’s ID before calling
delete_trigger_subscription. Remove subscriptions before deleting their
trigger instance or workflow.
Use update_trigger_instance({ id, config }) with a full replacement config to
change an existing trigger without replacing its id, endpoint, or subscriptions.
Its connection and enabled state stay intact. Endpoint and secret fields cannot
change; use reconnect_trigger_instance to swap a connection. Cron updates
recompute the next fire time from the new crontab/timezone. Updates require an
active instance; finish reconnect or registration before retrying an update.
Build a plugin
Section titled “Build a plugin”Connections work exactly the same way for a private plugin as for a built-in
integration: declare a connectionTypes entry in your own manifest, reference
it from a brick’s connections slots, and the SDK redeems and translates the
credential for you at invoke time. This is a better default than env-var
secrets — credentials are encrypted, rotatable, and never appear in your
plugin’s source or config.
Start with these code-backed references:
packages/plugin-sdk/README.mdis the SDK quickstart for manifests, brick handlers, typed connections, trigger lifecycle handlers, and event delivery.packages/e2e-fixture-plugin/src/index.tscontains copyable brick manifests, handlers, and request routing. The smallertrigger-fixture.tsandtrigger-fixture-worker.tsshow a minimal trigger manifest, lifecycle handler, state, and event delivery path. The adjacentbasic-bearer-fixture.ts/basic-bearer-worker.tsandoauth-fixture.ts/oauth-worker.tspairs exercise connection providers.packages/core-plugin/src/manifest.ts,bricks.ts, andworker.tsare the production-grade reference, with supporting storage and deduplication modules in the same directory. The core plugin declares no connection types; use the SDK README’sissue-tools/ Jira example for that shape instead.
The console shows each credential field’s JSON Schema description beneath its
input when creating or rotating a connection. Set it with .describe(...) on the
authored Zod field. Descriptions render as plain text, including URLs and Markdown;
fields without descriptions have no helper text.
Deploy every brick and trigger lifecycle handler at the exact endpoint in the
manifest, then publish from CI with the SDK CLI instead of maintaining a custom
upload script. @flowbrew/plugin-sdk@^0.8.0 provides:
# Validate deployment URLs without credentials or uploading:bunx flowbrew-plugin publish --manifest ./manifest.mjs \ --url send-message=https://my-plugin.example.com/send-message \ --url list-channels=https://my-plugin.example.com/list-channels --dry-run# In the release job, supply FLOWBREW_API_KEY through the secret environment:bunx flowbrew-plugin publish --manifest ./manifest.mjs \ --urls-json ./deployed-brick-urls.json --origin https://mcp.flowbrew.appRun with Node.js 24+. --manifest loads a local module exporting a ready
PluginManifest as default or manifest, or a sync/async function named
buildManifest(endpointUrls: Record<string, string>) (also accepted as the
default export). Resolution precedence is buildManifest, default, manifest.
Builders must declare all bricks even when a URL is missing. The CLI replaces
each brick’s endpoint by slug and validates the result. Keep trigger lifecycle
URLs in the authored manifest. Manifest modules execute as local code and should
avoid IO so dry runs stay offline.
--urls-json contains a JSON object mapping brick slugs to URL strings. You can
combine it with repeated --url slug=url flags in any order; every manifest
brick needs exactly one URL. Missing/extra slugs, duplicates (including conflicts
between JSON and flags), malformed flags, and insecure URLs fail with a nonzero
exit. HTTPS is required, with HTTP allowed only for local loopback validation
(localhost, 127.0.0.0/8, [::1]); hosted uploads still reject private endpoints.
The API key is read only from FLOWBREW_API_KEY. CLI diagnostics contain fixed
messages and HTTP status codes without echoing server bodies or caught errors;
literal-key redaction remains a defence in depth. Builders may be synchronous
or return a Promise/thenable; failures report only manifest builder failed.
Dry runs print the resolved manifest JSON, require no key, and skip
discovery/upload. Normal runs fail for a missing key before any network request.
--origin defaults to https://mcp.flowbrew.app. The CLI reuses the SDK’s
.well-known/flowbrew-plugin-configuration discovery to find pluginUploadUrl,
then uploads via authenticated HTTP and prints only {"ok":true} on success. This is
an HTTP publishing operation, separate from MCP. See the
SDK README for builder examples and the async
resolvePluginPublish export at @flowbrew/plugin-sdk/publish. publish uses
the default live channel; see plugin version channels
for candidate uploads and promotion using the existing commands.
Uploads always reject an existing version. The core-plugin publisher verifies a
duplicate using GET /plugins/{slug}/versions/{version} with the same API key.
This read-only route requires public publishing permission or private plugin
ownership and returns only { plugin: { id, slug, version, manifestSha256 } }.
The digest is SHA-256 of the schema-normalized JSON bytes, exposed by the SDK’s
pluginManifestSha256(manifest) helper. An identical digest skips publication;
different content requires a version bump, and any lookup failure fails the
publish. Missing/invalid keys return 401, insufficient publishing authorization
returns 403, and unknown plugins or versions return 404.
Lookup failure diagnostics include only fixed text, the HTTP status (or
unavailable), and a fixed reason: request_failed, http_error, invalid_json,
or invalid_response. They discard response content and original exceptions,
including validation paths and causes, to keep credentials out of logs.
Reference implementations
Section titled “Reference implementations”For a standard Cloudflare Workers-hosted plugin,
taskflow-hq/taskflow-slack-plugin
is the reference implementation. Copy its CI/CD workflow pattern as well
as its plugin code:
.github/workflows/ci.ymlruns type checks and tests on every push and pull request..github/workflows/deploy-staging.ymlgates deployment on tests, then deploys staging on pushes tomainand automatically publishes the manifest to the staging platform..github/workflows/deploy-production.ymlgates deployment on tests, then deploys production and automatically publishes the production manifest only on a version tag (v*).
If compliance requirements call for isolated per-customer compute — for
example, sensitive customer data must be handled in the customer’s own AWS
account instead of shared Cloudflare Workers — use the ControlDental plugin
repo as the precedent (private, in the controldental GitHub organization;
ask Jakub for access). Its .github/workflows/deploy.yml deploys AWS Lambda
with SST, with the same main → staging / git tag → production split. Only
production publishes a manifest, whose version comes directly from the git
tag name (a leading v is removed: v0.1.2 → 0.1.2). Its current test gate
covers staging; when adopting it, also gate production publishing on tests.
Hard rule: manifest publishing must be a CI step, gated by tests, with the published version tied to a git tag — never a commit SHA or an ad-hoc version scheme. Slack currently hardcodes its manifest version; when adopting its workflow pattern, derive the release version from the tag or fail CI if the manifest version does not match it. Manifest publishing must never be a manually run script during normal operation. A manual publish script is allowed only for local smoke-testing against a throwaway version, never for a real release.
Ignore the archived, no-longer-maintained standalone
taskflow-hq/taskflow-core-plugin repository entirely. The current core plugin
implementation lives in packages/core-plugin in
this monorepo.
Known gotchas
Section titled “Known gotchas”create_workflowcreates no executable version. Always complete at least one successful description-drivenupdate_workflowbeforestart_workflow.- Publication requires isolated mocked tests. These cover selected behavior, not all production effects. Callbacks and waitForEvent are unsupported and block publication through update_workflow. Validation has a 60-second total deadline.
- Plugin endpoints must be public HTTPS URLs with no embedded credentials or custom port, and their DNS must resolve to publicly routable addresses. Localhost and private-address endpoints are rejected during upload.
- A published plugin version is immutable. Re-uploading the same version works only for byte-identical manifest content; changed content needs a greater semantic version. Existing connection-type security semantics cannot be changed across versions.
- Every connection type needs a same-plugin validation brick with exactly one
required slot of that type.
configSchemaand fixedconfigmust either both be present or both be absent. - Trigger registration must persist the ingestion secret and credential generation atomically before acknowledging. The SDK explicitly warns that a bare eventually-consistent KV read/write pair is not a sufficient production state store.