Skip to content

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.

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.

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.

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.

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.

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.

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.

  1. Sign in to the web console at https://console.flowbrew.app (or your deployment’s console URL) and create or select a workspace.
  2. Create a workspace API key in API Keys. The raw key is shown only once.
  3. Point an MCP client at https://mcp.flowbrew.app/mcp (or your deployment’s own /mcp endpoint) 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:

Terminal window
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.

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.

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.

The normal order is shell, deployment, start, then status polling.

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.

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.

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.

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.

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.

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.

Choose a trigger definition from list_plugins, then call create_trigger_instance with its id as triggerDefinitionId and:

  • scope: "workspace" and projectId: null, or scope: "project" and the matching project ID;
  • configuration that matches the definition’s schema; and
  • either the required compatible connectionId or null for 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.

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.md is the SDK quickstart for manifests, brick handlers, typed connections, trigger lifecycle handlers, and event delivery.
  • packages/e2e-fixture-plugin/src/index.ts contains copyable brick manifests, handlers, and request routing. The smaller trigger-fixture.ts and trigger-fixture-worker.ts show a minimal trigger manifest, lifecycle handler, state, and event delivery path. The adjacent basic-bearer-fixture.ts / basic-bearer-worker.ts and oauth-fixture.ts / oauth-worker.ts pairs exercise connection providers.
  • packages/core-plugin/src/manifest.ts, bricks.ts, and worker.ts are 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’s issue-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:

Terminal window
# 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.app

Run 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.

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:

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.

  • create_workflow creates no executable version. Always complete at least one successful description-driven update_workflow before start_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. configSchema and fixed config must 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.