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 entire FAL codegen and node system was introduced in commit d1491abf (“add claude agent package”), which added all src/configs/ files, the codegen pipeline, and the factory. That commit is the canonical reference for the shape of every config file and 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/fal-nodes/src/fal-factory.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.