Custom Node Development
A FluidGrids node is a small self-contained service. It exposes a GraphQL API, and the workflow engine calls it while a run executes. You build one with the fluidgrids-node-sdk Python package. It generates a working node, checks it against the node contract, runs it the way the platform does, and packages it for the Developer Portal. You then publish it from Developer Portal > Applications. After review, it appears in the Node Store for every workspace.
If you only need a workflow to call an API, you don't need a node: the built-in HTTP Request node already handles authentication, retries and pagination. Build a node when you want a first-class builder experience, with its own icon, a form for each operation, typed results, and use as an AI Agent tool.
Install the SDK
The SDK needs Python 3.11 or later. smoke also needs Docker.
pip install fluidgrids-node-sdk
fluidgrids-node --version
| Command | What it does |
|---|---|
fluidgrids-node new <name> | Generate a working node in <name>_node/1.0.0/ |
fluidgrids-node validate <dir> [--strict] | Check the manifest, the GraphQL contract and the package layout |
fluidgrids-node smoke <dir> | Build the node's container, start it and call every operation |
fluidgrids-node register <dir> --write | Regenerate register.gql from manifest.json |
fluidgrids-node package <dir> | Write the zip you upload, and print its SHA-256 |
fluidgrids-node schema | Print the JSON Schema for manifest.json |
1. Scaffold the node
fluidgrids-node new acme_crm --title "Acme CRM" --author "Acme Inc."
cd acme_crm_node/1.0.0
This creates a complete node that already passes every check:
acme_crm_node/1.0.0/
├── manifest.json # the contract: identity + operations[]
├── acme_crm_sdl.gql # GraphQL SDL, one field per operation
├── acme_crm_gql.py # resolvers: every field goes through dispatch()
├── acme_crm_actions_ops.py # handlers: the real work, one per operation
├── adapter_supply.py # exposes an operation to an AI Agent as a tool
├── config.py # settings from the environment
├── server.py, router.py # the app (/graphql) and its health route
├── test_acme_crm_node.py # unit tests
├── requirements.txt # pinned runtime dependencies
├── Dockerfile, docker-compose.yml, .env.example
├── register.gql # registerNode mutation, generated from the manifest
└── README.md
It ships with three example operations to replace with your own:
formatTextneeds no connection. It shows how to validate input.getResourcereads the user's connection and calls your API. It shows how credentials arrive.supplyToollets an AI Agent use the node's operations as tools.
2. Write your operations
The manifest is the contract. The builder, the Node Store and the workflow engine all read manifest.json. Every operation in it must have three parts that match:
-
A manifest entry. It has a
namein camelCase and atype(queryfor reads,mutationfor writes). It also has adescription, itsinput_fieldsandoutput_fields, anadaptive_card, and itscredentialAdapters. -
An SDL field with exactly this signature:
myOperation(context: ExecutionContextInput, state: JSON): MyOperationResult!The result type always has an
error: Stringfield. -
A handler in
<name>_actions_ops.py. Add it toHANDLERS, and add its empty result toEMPTY_RESULTS.
The rules a handler follows:
-
Never raise. Return the result with
errorset. The scaffold'sdispatch()turns any exception into an error result, so a bad input can't crash a run. -
Validate
state. The engine sends the operation's inputs as untyped JSON. Reject bad input with anInput Error: ...message. -
Read credentials from
context. Never store them. The engine passes the user's connection incontext.credentialsunder the adapter name you declared incredentialAdapters. It is one flat dictionary, whatever connection method the user chose:Connection method Fields OAuth 2.0 / bearer accessToken,refreshToken,tokenType,expiresAt,scopeAPI key apiKey(also asaccessToken), plus any extra form fieldsBasic auth username,passwordCustom form the form's own fields ( host,port, …)When the connection method says how the credential is sent, the dictionary also carries
inject, the ready-made request authentication, for example{"headers": {"Authorization": "Bearer …"}}. Attachinjectwhen it is present, and fall back toaccessTokenorapiKeywhen it is not. The scaffold'srequest_auth()does exactly this. -
Fix the destination. Take the API host from your configuration, never from workflow input. Then a workflow can't send a user's credential to a server of its choosing (see
resource_url()in the scaffold).
The configuration panel
Each operation's adaptive_card is an AdaptiveCard 1.5. The builder shows it as that operation's form. Bind each input to an input field as "id": "args.state.<field>" and "value": "${<field>}". Add a defaultValue map and one Action.Submit. validate warns about an input that is not bound to an input_fields entry. See Node Property Panels.
Files
Nodes exchange file bytes as base64 strings. Mark an output field that carries file bytes with "format": "binary". You can name its MIME type and file name fields with "mimeTypeField" and "fileNameField". FluidGrids then stores the file in the run owner's Files library and passes a file reference along the workflow. A later node's base64 input still receives the bytes.
3. Test it
pip install -r requirements.txt pytest
pytest # unit tests
fluidgrids-node validate . --strict # manifest, SDL and package checks
fluidgrids-node smoke . # container + every operation
validatechecks the manifest against the contract and every operation against the SDL. It also checks that the package has what the platform build needs:requirements.txtandserver.pyfor a Python node. Errors always fail.--strictalso fails on warnings (a missing panel, a result withouterror, a staleregister.gql). A new node should pass--strict.smokebuilds your Dockerfile'sdevelopmentstage, starts the container, and calls every operation with an empty input. An operation passes when it answers with a typed result: the standard "No … credentials in execution context" error counts as a pass. It fails on a GraphQL or resolver error. Usefluidgrids-node smoke . --url http://localhost:6700against a node you already have running.
To run the node while you develop, use docker compose up -d (with hot reload). Then open http://localhost:<port>/graphql. The compose file joins the shared nodenetwork Docker network; create it once with docker network create nodenetwork. After you edit manifest.json, run fluidgrids-node register --write . to bring register.gql up to date.
4. Package and publish
fluidgrids-node package .
# Packaged store/[email protected]: ./store-acme_crm-1.0.0.zip
# sha256: 3f1c…
package runs validate first and refuses a node with errors. The zip has your node's files at its root. It leaves out .env files, and also your Dockerfile, because the platform builds every node with its own image definition and discards any Dockerfile in the package. The zip is deterministic: the same files always give the same SHA-256.
Then, in FluidGrids:
- Open Developer Portal > Applications and create an application of type Node. Fill in its store listing: description, category, icon and visibility.
- Open Versions > New version. Upload the zip and
manifest.json, and set the version to the manifest'snodeVersion. - FluidGrids validates the package as soon as the version is created, then builds the node in a sandbox. The Versions page shows the validation result next to each version: Validating package…, Package validated, or Validation failed with the list of problems. A package that fails validation is never built. When the build is ready, Test in Workspace installs the node into one of your workspaces, so you can run it in a workflow before anyone else can see it.
- Submit for review. A version can only be submitted once its manifest and package have passed validation. A FluidGrids admin reviews the version, with the same validation report in front of them. After approval, the node is listed in the Node Store, and workspaces can add it to their workflows.
If you upload a new zip to a draft version, FluidGrids validates and builds it again. Until that finishes, the version can't be submitted.
What FluidGrids checks when you upload
The checks run on the platform, so they apply however you built the zip. fluidgrids-node validate --strict catches most of them on your machine first.
Errors stop the build and block submission:
| Area | Check |
|---|---|
| Identity | nodeType is <namespace>/<name> in lowercase. nodeVersion is a semantic version and equals the version you are publishing. name is set (at most 100 characters). |
| Endpoint | endpointUrl is an http(s) URL with an explicit port between 1024 and 65535. Port 9001 is reserved for the sandbox. healthCheckUrl, when present, uses the same port. |
| Operations | At least one operation. Each has a unique name that is a valid GraphQL field name and a type of query, mutation, subscription or trigger. Each input_fields entry is an object with a type; required is a boolean. Each output_fields entry is a type name or an object. |
| Connections | Each credentialAdapters entry has a name and a lowercase integration provider key. |
| AI Agent use | When supportsAdapterMode is true, adapterOutputType is set and looks like ai_<kind> (for example ai_tool). Adapter ports (nodeAdapters) have a unique name and a type. |
| Package | The zip is readable and unpacks to at most 64 MB. No entry escapes the package root. It has manifest.json, and either requirements.txt with server.py (Python) or package.json with a start script and bun.lock (Bun). The package's manifest.json matches the manifest you entered for the version (nodeType, nodeVersion, endpointUrl). |
| Code | No process spawning (subprocess, os.system, os.popen, os.exec*, os.spawn*, os.fork, child_process, Bun.spawn) and no native code (ctypes, cffi, pty, bun:ffi). No compiled binaries (.so, .dll, .pyc and similar) and no vendored dependency folders (node_modules, site-packages, .venv): declare dependencies in requirements.txt or bun.lock instead. |
Warnings don't block anything, but the reviewing admin sees them and decides:
- dynamic code (
eval,exec,compile,new Function, thevmmodule), dynamic imports (__import__,importlib) and unsafe deserialization (pickle,marshal,shelve); - a blocked construct inside a test file (
test_*.py,*.test.ts, atests/folder), which is allowed only because the server never loads it; - an input field type the builder doesn't render natively, or an input field name that isn't a plain identifier (such as
@odata.etag); - an operation or node without a description;
- a
Dockerfile,hooks.pyorentrypoint.shin the zip: the platform supplies its own and ignores yours.
Each problem names where it is, for example manifest.json:operations[2].name or server.py:14, so you can find it quickly.
To ship a change, raise nodeVersion (and the version directory name), then package and upload a new version. Workflows keep the version they were built with until someone moves them to the new one.
Browsing the catalog
The CLI can list and inspect nodes that are already in the catalog:
fluidgrids nodes list # list available nodes
fluidgrids nodes get <type> # inspect a node's manifest
Next steps
- Node Property Panels: richer configuration panels
- Nodes: the built-in and Node Store catalog
- Examples: sample workflows and nodes