Queue Finished Clips in Node-RED After Using a Free AI Video Generator

Queue Finished Clips in Node-RED After Using a Free AI Video Generator

A file appearing in a downloads folder tells you that a write has started. A reviewer needs a different signal. They need the clip to be fully saved and available under a stable name.

This Node-RED flow watches a local handoff directory and records a clip when a separate completion marker appears. It also has a manual Scan inbox button. If Node-RED was stopped during a handoff, the scan picks up markers that are still in the directory.

Illustration of the handoff: write the clip first, then place its completion marker.

Put the completion marker beside the clip

Use a lowercase ID for each version. If the clip is cart-v01.mp4, its marker is cart-v01.ready. Save or copy the full MP4 into the inbox first. Only create the marker once the copy has finished. An empty marker file is enough.

Keep a revised clip under a new ID such as cart-v02. The ID is the unit of deduplication. Once cart-v01.json exists in the review folder, replacing the contents of cart-v01.mp4 would change the file a reviewer sees behind that same record.

Make two local directories and pass their absolute paths to Node-RED as environment variables. The commands below work on macOS and Linux.

mkdir -p "$HOME/clip-inbox" "$HOME/clip-review"
export CLIP_INBOX="$HOME/clip-inbox"
export CLIP_REVIEW="$HOME/clip-review"
node-red

Use a local Node-RED instance with the editor bound to loopback. Set uiHost: "127.0.0.1" in its settings file if needed. The flow's Function node needs functionExternalModules: true for its Node.js fs and path imports. Node-RED environment variables The two directories are intended for trusted local writers. In particular, the marker is a producer's confirmation that the file is ready. This sample checks that the named file exists and has bytes. Verify playback separately if your handoff needs a decoder check.

Import the flow

Open Node-RED's Import dialog and paste the JSON below. Watch clip inbox sends file changes to the Function node. The node checks the marker and writes one JSON record per ID. Review status shows whether the node registered the clip, skipped a repeat, or found the clip missing or empty. Scan inbox runs once after deploy and can also be triggered manually.

The imported flow in Node-RED 4.1.1. Both the Watch node and the inbox scan feed the same registration check.

[
  {
    "id": "a050000000000000",
    "type": "tab",
    "label": "Clip handoff",
    "disabled": false,
    "info": ""
  },
  {
    "id": "a050000000000001",
    "type": "watch",
    "z": "a050000000000000",
    "name": "Watch clip inbox",
    "files": "${CLIP_INBOX}",
    "recursive": false,
    "x": 180,
    "y": 120,
    "wires": [
      [
        "a050000000000003"
      ]
    ]
  },
  {
    "id": "a050000000000002",
    "type": "inject",
    "z": "a050000000000000",
    "name": "Scan inbox",
    "props": [
      {
        "p": "topic",
        "v": "rescan",
        "vt": "str"
      }
    ],
    "once": true,
    "onceDelay": 1,
    "repeat": "",
    "crontab": "",
    "x": 170,
    "y": 200,
    "wires": [
      [
        "a050000000000003"
      ]
    ]
  },
  {
    "id": "a050000000000003",
    "type": "function",
    "z": "a050000000000000",
    "name": "Check marker and register",
    "func": "const inbox = env.get(\"CLIP_INBOX\");\nconst queue = env.get(\"CLIP_REVIEW\");\nif (!inbox || !queue || path.resolve(inbox) === path.resolve(queue)) {\n    node.error(\"Set CLIP_INBOX and CLIP_REVIEW to separate local folders.\", msg);\n    return null;\n}\n\nfunction register(name) {\n    const match = /^([a-z0-9][a-z0-9-]{0,63})\\.ready$/.exec(name);\n    if (!match) return null;\n    const id = match[1];\n    const file = id + \".mp4\";\n    const markerPath = path.join(inbox, name);\n    const clipPath = path.join(inbox, file);\n\n    try {\n        if (!fs.lstatSync(markerPath).isFile()) return null;\n    } catch (error) {\n        if (error.code === \"ENOENT\") return null;\n        throw error;\n    }\n\n    try {\n        const clip = fs.lstatSync(clipPath);\n        if (!clip.isFile() || !clip.size) {\n            return { id, file, status: \"clip missing or empty\" };\n        }\n    } catch (error) {\n        if (error.code === \"ENOENT\") return { id, file, status: \"clip missing or empty\" };\n        throw error;\n    }\n\n    const record = { id, file, registeredAt: new Date().toISOString() };\n    const temporaryDir = fs.mkdtempSync(path.join(queue, \".pending-\"));\n    try {\n        const temporary = path.join(temporaryDir, \"record.json\");\n        fs.writeFileSync(temporary, JSON.stringify(record) + \"\\n\", { flag: \"wx\" });\n        try {\n            fs.linkSync(temporary, path.join(queue, id + \".json\"));\n        } catch (error) {\n            if (error.code === \"EEXIST\") return { id, file, status: \"duplicate skipped\" };\n            throw error;\n        }\n        return { ...record, status: \"registered\" };\n    } finally {\n        fs.rmSync(temporaryDir, { recursive: true, force: true });\n    }\n}\n\ntry {\n    let names;\n    if (msg.topic === \"rescan\") {\n        names = fs.readdirSync(inbox);\n    } else if (msg.type === \"file\" && typeof msg.filename === \"string\") {\n        names = [path.basename(msg.filename)];\n    } else {\n        return null;\n    }\n\n    const results = names.map(register).filter(Boolean);\n    return [results.map(payload => ({ payload }))];\n} catch (error) {\n    node.error(error.message, msg);\n    return null;\n}",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [
      {
        "var": "fs",
        "module": "fs"
      },
      {
        "var": "path",
        "module": "path"
      }
    ],
    "x": 450,
    "y": 160,
    "wires": [
      [
        "a050000000000004"
      ]
    ]
  },
  {
    "id": "a050000000000004",
    "type": "debug",
    "z": "a050000000000000",
    "name": "Review status",
    "active": true,
    "tosidebar": true,
    "console": true,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "x": 690,
    "y": 160,
    "wires": []
  }
]

Each record is written to a temporary directory within the review folder. The function then makes the finished record visible under the clip ID using a hard link, a second filesystem name for the same file. If the final name already exists, the second operation fails with EEXIST and the existing record stays intact. Keep the review folder on a local filesystem that supports hard links and use the visible .json records as the queue. A restart preserves the IDs because the records are on disk.

Try a normal handoff, a repeat and an early marker

With the flow deployed, run these commands one at a time using a short MP4 you own. Pause after the first touch until the new record appears in CLIP_REVIEW.

cp ./sample.mp4 "$CLIP_INBOX/cart-v01.mp4"
touch "$CLIP_INBOX/cart-v01.ready"
touch "$CLIP_INBOX/cart-v01.ready"
touch "$CLIP_INBOX/missing-v01.ready"

On macOS with Node-RED 4.1.1 and Node.js 24.18.0, the first marker wrote one record. Updating it again left the original record's contents and timestamp intact. The marker without an MP4 produced a missing-clip status and no review record.

Inbox event Review-folder result
Clip and matching marker cart-v01.json appears with the ID, filename and registration timestamp.
Updated marker for the same ID The existing cart-v01.json is preserved.
Marker arrives early No record appears. Save the missing MP4, then update the marker again or use Scan inbox.

Illustration of the three handoff cases: accept the first match, skip a duplicate and report a missing clip.

Treat the marker and clip as a pair until review finishes. If you need to move completed work out of the inbox, remove or move the marker with the clip. Keep the review record as long as you want that ID to stay reserved.

If you want a small replaceable clip for this exercise, the Flaq AI free AI video generator describes its free preview as 480p with no signup required. Save a copy you have permission to use, then run the same file handoff. The Node-RED flow only needs the downloaded file and its marker.

This guide is by the Flaq AI team and references our own tool.