FAL nodes in NodeTool are generated, not hand-written. A codegen pipeline fetches each endpoint’s OpenAPI schema from fal.ai, converts it to a manifest, and the runtime loads node classes from that manifest.
Audience: coding agents and contributors adding new FAL endpoints or fixing generated node behavior.
TL;DR
- Add the endpoint ID +
NodeConfigto the rightsrc/configs/<category>.tsfile inpackages/fal-codegen/. - Run
npm run generate:falfrom the repo root. - Run
npm run build:packages(fal-nodes loads fromdist/). - Run
npm run lint --workspace=packages/fal-codegenandnpm run test --workspace=packages/fal-nodes. - Open a PR — never commit manual edits to
fal-manifest.json.
Where things live
| Concern | Path |
|---|---|
| Endpoint configs (source of truth) | packages/fal-codegen/src/configs/<category>.ts |
| Config type definitions | packages/fal-codegen/src/types.ts |
Schema fetcher (caches to .codegen-cache/) |
packages/fal-codegen/src/schema-fetcher.ts |
| Schema → NodeSpec parser | packages/fal-codegen/src/schema-parser.ts |
| Config overrides applier | packages/fal-codegen/src/node-generator.ts |
| Codegen entry point | packages/fal-codegen/src/generate.ts |
| Generated manifest (do not edit) | packages/fal-nodes/src/fal-manifest.json |
| Runtime node class factory | packages/fal-nodes/src/fal-factory.ts |
| FAL API call utilities | packages/fal-nodes/src/fal-base.ts |
| Package entry point | packages/fal-nodes/src/index.ts |
| Pricing bundles (generated) | packages/fal-nodes/src/generated/ |
| High-level provider (text→image, TTS, etc.) | packages/runtime/src/providers/fal-provider.ts |
| Manifest model loading | packages/runtime/src/providers/manifest-models.ts |
How FAL nodes are generated
src/configs/<category>.ts ← you edit this
↓
schema-fetcher.ts fetches https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=...
caches each schema in packages/fal-codegen/.codegen-cache/<sha>.json
↓
schema-parser.ts converts OpenAPI → NodeSpec (inputFields, outputType, enums)
↓
node-generator.ts merges NodeConfig overrides (className, fieldOverrides, enumOverrides…)
↓
generate.ts writes packages/fal-nodes/src/fal-manifest.json
↓
fal-factory.ts creates runtime node classes from manifest at startup
The root generate:fal script runs in strict mode by default: if any configured endpoint cannot be fetched or parsed, generation fails. Remove stale endpoints from the config or fix them before regenerating.
Never edit fal-manifest.json directly. Changes are overwritten on the next npm run generate:fal.
Add a new FAL endpoint
1. Find the right config file
Pick the file in packages/fal-codegen/src/configs/ that matches the endpoint’s modality:
| Endpoint type | Config file |
|---|---|
| text → image | text-to-image.ts |
| image → video | image-to-video.ts |
| text → video | text-to-video.ts |
| text → speech | text-to-speech.ts |
| image → image | image-to-image.ts |
| other | matching <input>-to-<output>.ts, or unknown.ts |
2. Add a NodeConfig entry
Open the chosen config file and add a key inside configs whose value is a NodeConfig:
// packages/fal-codegen/src/configs/text-to-image.ts
import type { ModuleConfig } from "../types.js";
export const config: ModuleConfig = {
configs: {
// ... existing entries ...
"fal-ai/my-new-model": {
className: "MyNewModel",
docstring: "One sentence describing what the model does.",
tags: ["image", "generation", "text-to-image", "txt2img"],
useCases: [
"Generate images from text descriptions",
"Create concept art from prompts"
],
fieldOverrides: {
prompt: {
description: "The text prompt to generate an image from"
},
image_size: {
propType: "enum",
enumRef: "ImageSizePreset", // reuse a sharedEnum if available
default: "landscape_4_3",
description: "Output image size preset"
},
seed: {
propType: "int",
default: -1,
description: "Seed for reproducible results. Use -1 for random"
}
}
}
}
};
NodeConfig fields (all optional — only add what needs overriding):
| Field | Type | Purpose |
|---|---|---|
className |
string |
PascalCase class name for the node |
docstring |
string |
Node description shown in the UI |
tags |
string[] |
Used for node search |
useCases |
string[] |
Shown in node detail panel |
fieldOverrides |
Record<string, Partial<FieldDef>> |
Override parsed field properties (type, default, description…) |
enumOverrides |
Record<string, string> |
Rename enums: { ImageSize: "ImageSizePreset" } |
enumValueOverrides |
Record<string, Record<string, string>> |
Rename enum values |
FieldDef override keys: propType, default, description, enumRef, min, max.
Valid propType values: "str" "float" "int" "bool" "image" "video" "audio" "enum" "list[image]" "list[video]" "list[audio]" "dict[str, any]".
3. (Optional) Add a shared enum
If the endpoint introduces a new enum that other nodes in the same module will reuse, add it to the sharedEnums block:
export const config: ModuleConfig = {
sharedEnums: {
MyAspectRatio: {
name: "MyAspectRatio",
values: [
["SQUARE", "1:1"],
["WIDESCREEN", "16:9"],
["PORTRAIT", "9:16"]
],
description: "Aspect ratio for output"
}
},
configs: { /* ... */ }
};
Then reference it via enumRef: "MyAspectRatio" in a fieldOverrides entry.
4. Run codegen
# From the repo root:
npm run generate:fal
This fetches the schema (or reads from .codegen-cache/ if cached), writes packages/fal-nodes/src/fal-manifest.json, and updates pricing bundles under packages/fal-nodes/src/generated/ when FAL_API_KEY is set.
To force a fresh schema fetch (bypass cache):
npm run generate --workspace=packages/fal-codegen -- --strict --no-cache
Verify
Run these in order after any config change:
# 1. Regenerate the manifest
npm run generate:fal
# 2. Check for TypeScript errors in codegen
npm run lint --workspace=packages/fal-codegen
# 3. Run codegen tests
npm run test --workspace=packages/fal-codegen
# 4. Build fal-nodes (loads manifest from dist/)
npm run build:packages
# 5. Run fal-nodes tests (exercises the generated node classes)
npm run test --workspace=packages/fal-nodes
# 6. Smoke-test one generated node (replace type and props as appropriate)
npm run dev:nodetool -- node run fal.text_to_image.FluxDev \
--props '{"prompt": "a red apple on a white table"}' \
--no-secrets
The node type follows the pattern fal.<module_name>.<ClassName>, where module_name comes from the config file key in src/configs/index.ts (e.g. text_to_image).
Before committing, inspect the manifest diff:
git diff packages/fal-nodes/src/fal-manifest.json | head -80
Confirm the new entry appears with the expected className, inputFields, and outputType.
How past commits did it
The FAL codegen and node system was built over a run of commits on 2026-03-12, starting with 09bb4d60 (“feat(fal-codegen): add SchemaFetcher with SHA-256 caching and tests”) and 2e0df019 (“feat(fal-codegen): add SchemaParser and tests”); 54ee1cce scaffolded src/configs/, and the Python configs were ported over in batches (7b6a609f, 8af2c275). The manifest-driven factory came later, in 85787419 (“feat(fal): replace codegen with manifest-driven dynamic registry”) — that commit is the reference for the runtime loading path.
Subsequent behavioral fixes follow a clear split:
ff5824a6— fixedschema-parser.tsto collapse single-asset wrapper structs (list[ImageInput]) tolist[image]with anestedAssetKeyhint; the fix lived in codegen, not the manifest.2997a678— added FAL billing reconciliation; changes touchedfal-base.ts,fal-factory.ts, andfal-billing.tsinfal-nodes/.
Both commits follow the rule: parser/codegen bugs go in packages/fal-codegen/; runtime bugs go in packages/fal-nodes/; never patch fal-manifest.json.
Fixing a wrong input or output on a generated node
| Problem | Where to fix |
|---|---|
| Field parsed with wrong type | packages/fal-codegen/src/schema-parser.ts |
| Field needs a different default or description | fieldOverrides in the endpoint’s config entry |
| Enum name conflicts across nodes | enumOverrides in the config entry |
Asset input defaults to "" instead of AssetRef |
defaultForPropType() in packages/node-sdk/src/manifest-node-values.ts |
| Audio/video output missing preview | set metadataOutputTypes in the factory, not outputTypes |
| API call behavior (retry, upload, mapping) | packages/fal-nodes/src/fal-base.ts |
Contributing
PRs are welcome. Open one at https://github.com/nodetool-ai/nodetool. Before pushing, run:
npm run check # typecheck + lint + test across all workspaces
Join the discussion on Discord.