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

**URL:** <https://discourse.nodered.org/t/queue-finished-clips-in-node-red-after-using-a-free-ai-video-generator/102138>\
**Category:** Vendors\
**Created:** [12 November 2026 16:12 UTC](https://discourse.nodered.org/t/queue-finished-clips-in-node-red-after-using-a-free-ai-video-generator/102138 "2026-11-12T16:12:25Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![flaq-ai](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/flaq-ai/32/108885_2.png) [@flaq-ai](https://discourse.nodered.org/u/flaq-ai)\
**Post date:** [12 November 2026 16:12 UTC](https://discourse.nodered.org/t/queue-finished-clips-in-node-red-after-using-a-free-ai-video-generator/102138/1 "2026-11-12T16:12:25Z")

</div>

# 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.

 ![A blank media card enters a tray. An adjacent tray holds another card with a purple tab.](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/5/d/5d03903e5169694010baab4a3c3a1e7912ce5410.jpeg)

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.

```sh
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](https://nodered.org/docs/user-guide/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 Node-RED editor shows Watch clip inbox and Scan inbox connected to Check marker and register, followed by Review status.](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/4/e/4e70a688100b84d717fd3ca2f3465cb9fa87c702.png)

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

```json
[
  {
    "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`.

```sh
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**. |

 ![Three illustrated trays show a clip with one violet tab, a clip with two tabs, and an empty slot with one tab.](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/3/0/300eaca3619d9d1ac95ba1217491e581a1d01308.jpeg)

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](https://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.
