Extending Flowcrafter
The front door to Flowcrafter's extension API — the seams, the two registries, and how gating works.
Flowcrafter is a visual workflow builder for Gravity Forms, and almost everything it does is open to extension.
The whole surface hangs off a handful of WordPress hooks and one JavaScript global (window.flowcrafter). Attach your hooks at file-load time and your callbacks simply never fire when Flowcrafter is absent.
These reference samples show two different seams: a new node type on the canvas, and extra fields added to an existing node. They now live under docs/examples/extension-samples/ and are not loaded by core; the screenshots show them copied into a development integration plugin.


Every way in is a hook or filter. The registration hooks each hand you the relevant registry class name as a string, and you call its static ::register().
| Seam | Hook | Type | When it fires | What you do |
|---|---|---|---|---|
| Add a new node type | flowcrafter_register_nodes |
action | During init, after core nodes register | $registry_class::register( 'MyNodeType', My_Handler::class ) |
| Advertise an offer on a built-in node | flowcrafter_register_node_extensions |
action | Right after flowcrafter_register_nodes |
$registry_class::register( 'HTTPRequestNode', 'my-slug', [ ...offer... ] ) |
| Activate an extension provider | flowcrafter_register_node_extension_providers |
action | Right after the offers hook | $registry_class::register( new My_Provider() ) |
| Register a template / recipe | flowcrafter_templates |
filter | When the templates list is read | Return $templates with your recipe appended |
| Enqueue a builder bundle | flowcrafter_editor_scripts |
action | After the builder script is enqueued | wp_enqueue_script() depending on flowcrafter-editor |
| Advertise a locked node in the picker | flowcrafter_advertised_node_types |
filter | On the node-types discovery call | Return $advertised with a locked node row (push only while your plugin is inactive) |
| Add capability strings for a node | flowcrafter_node_capabilities |
filter | When node capabilities are gathered | Return $caps with custom strings |
| Feed-addon compatibility opt-in | flowcrafter_addon_is_compatible |
filter | Feed discovery | Return a bool for whether a GF feed add-on works as a feed node (payment add-ons default to false) |
| Add builder localize data | flowcrafter_editor_localize_data |
filter | When the builder JS global is built | Add data (e.g. node icons) to the flowcrafterEditor global |
The three registration actions fire in a fixed order, and each passes you a class name:
do_action( 'flowcrafter_register_nodes', Flowcrafter\Node_Registry::class );do_action( 'flowcrafter_register_node_extensions', Node_Extension_Registry::class );do_action( 'flowcrafter_register_node_extension_providers', Node_Extension_Provider_Registry::class );Beyond these builder-time seams, there’s a family of per-node execution filters named flowcrafter_node_{node_type}_{event} that fire during a run — the place to change what a node actually does at runtime (replace an HTTP request, swap out an entry query, override a condition’s yes/no). Those live with the execution model.
Some of them let an extension take over a node’s work: flowcrafter_node_http_request_node_request adopts your HTTP request in place of the one the node built, and flowcrafter_node_get_entries_node_entry_ids and flowcrafter_node_get_entries_node_entries supply a Get Entries node’s matches instead of its built-in query.
Hooks lists every action and filter, each linking to its parameters and examples.
Adding a node and extending a node are different jobs, backed by different registries. Pick the one that matches your intent.
| You want to… | Registry | You register | Result |
|---|---|---|---|
| Ship a brand-new capability that stands alone | Node_Registry |
A Node_Handler class under a type_key |
A new node in the palette |
| Make an existing node do more | Node_Extension_Registry + Node_Extension_Provider_Registry |
An offer (metadata) plus a provider (fields/schema/validation/execution) | Extra settings and behavior grafted onto a core node |
A rough rule: if a user would think of it as its own step on the canvas, it’s a node. If it’s “the HTTP Request node, but with OAuth,” it’s an extension.
flowchart TD
A[Third-party plugin loads] --> B{New capability or enhance existing?}
B -->|Stands alone| C[flowcrafter_register_nodes]
C --> D[Node_Registry Node_Handler]
B -->|Enhance a core node| E[flowcrafter_register_node_extensions]
E --> F[Node_Extension_Registry offer]
F --> G[flowcrafter_register_node_extension_providers]
G --> H[Node_Extension_Provider_Registry provider]
Flowcrafter shows locked capabilities in the builder even when the plugin that powers them isn’t there yet. That’s how the upsell works. There are two flavors of lock, and they answer different questions.
Product-lock — “you need to install/activate a product.” An offer registered in Node_Extension_Registry is metadata only; it’s seeded available: false. The moment a matching provider is registered in Node_Extension_Provider_Registry for the same (base_node_type, slug) pair, the offer flips to available and its real settings render. No provider means the node stays in upsell mode. Whole node types work the same way through flowcrafter_advertised_node_types — advertised as locked while your plugin is inactive, replaced by the real handler the moment it activates.
Capability-lock — “you need a WordPress permission.” A handler that implements Requires_Capability is gated at every REST entry point. The node shows as locked to users who can’t manage it (the PHP Code node is the built-in example), and its real settings fields are hidden from the wire until the permission check passes.
| Lock kind | Trigger | On the wire |
|---|---|---|
| Product-lock (extension) | No provider for (base_node_type, slug) |
extensions[i].available = false |
| Product-lock (whole node) | Advertised via flowcrafter_advertised_node_types |
node-level available: false + requires + product_url + upsell, and an optional tone (ai, actions, logic …) so the card and picker show its category before the add-on is on |
| Capability-lock | Handler implements Requires_Capability and the user can’t manage it |
node-level available: false + requires: "{label} permission" |
An offer is the metadata behind a product-lock upsell — the add-on upsell card in a base node’s settings and the upsell modal when the product isn’t active. You register it against a (base_node_type, slug) pair with a handful of keys:
| Key | Meaning |
|---|---|
slug |
Stable extension slug; must match the provider’s get_slug() |
base_node_type |
The node being enhanced |
label |
Product / enhancement display name |
plugin_slug |
WordPress plugin slug for install / activate |
product_line |
gravity_connect or gravity_perks — routes the upsell to the right pricing |
product_url |
Product documentation URL. Destination for missing-pricing fallback links and product-information links. |
value_prop |
One-line “what you get” for the callout |
pitch |
Longer paragraph for the upsell modal |
features |
Bulleted feature list in the upsell |
When a provider is active, Flowcrafter also folds render_mode, settings_fields, settings_schema, and output_schema onto the offer from that provider — so the same record drives both the locked pitch and the live settings. See React extensions for the provider-and-panel side of this.
Prices aren’t Flowcrafter’s to filter. They come live from the Gravity Wiz store by way of Spellbook, so the hooks that change them (spellbook_pricing, spellbook_pricing_endpoint) belong to Spellbook.
type_key, and opt into the contract interfaces that grant powers like triggers, entry resolution, and collections.window.flowcrafter exports, registerNodeExtension, the NodeExtensionSettingsProps contract, and composition claims.