# Creating a New Custom Module in ApraPipes: A Complete Developer Guide

> Learn how to create a new custom module in ApraPipes. This guide covers defining C++ classes, registering modules, and rebuilding for JSON pipelines and Node.js API integration.

- Repository: [Apra Labs/aprapipes](https://github.com/apra-labs/aprapipes)
- Tags: how-to-guide
- Published: 2026-02-25

---

**To create a custom module in ApraPipes, you must define a C++ class derived from `Module` with a corresponding `*Props` class, register it in [`ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/ModuleRegistrations.cpp) using the declarative registry, and rebuild the library to make it available via JSON pipelines or the Node.js API.**

ApraPipes is a high-performance computer vision and image processing framework developed by Apra Labs. Creating a new custom module in ApraPipes allows you to extend the framework's capabilities with proprietary image processing algorithms while maintaining compatibility with the declarative JSON pipeline syntax and Node.js bindings.

## Step 1 – Define the Module Implementation and Props Class

Every module requires two components: a properties class (`*Props`) that inherits from `ModuleProps`, and the module class itself that inherits from `Module`. The properties class handles serialization and dynamic property updates, while the module class implements the processing logic.

### Creating the Header File

Place your header in `base/include/<YourModule>.h`. The following example demonstrates a `MyBlur` module with a configurable radius property:

```cpp
// base/include/MyBlur.h
#pragma once
#include "Module.h"
#include "declarative/PropertyMacros.h"

class MyBlurProps : public ModuleProps {
public:
    int radius = 3;                // default value

    // --------------------------------------------------------------------
    // Property binding for declarative pipelines
    // --------------------------------------------------------------------
    template<typename PropsT>
    static void applyProperties(
        PropsT& props,
        const std::map<std::string, apra::ScalarPropertyValue>& values,
        std::vector<std::string>& missingRequired)
    {
        apra::applyProp(props.radius, "radius", values, true, missingRequired);
    }

    // Optional dynamic‑property support (runtime change)
    static std::vector<std::string> dynamicPropertyNames() { return {"radius"}; }

    apra::ScalarPropertyValue getProperty(const std::string& name) const {
        if (name == "radius") return static_cast<double>(radius);
        throw std::runtime_error("Unknown property: " + name);
    }
    bool setProperty(const std::string& name, const apra::ScalarPropertyValue& value) {
        if (name == "radius") { radius = static_cast<int>(std::get<double>(value)); return true; }
        return false;
    }
};

class MyBlur : public Module {
public:
    MyBlur(MyBlurProps p);
    ~MyBlur() override = default;

    bool init() override;
    bool term() override;
    void setProps(MyBlurProps& p);
    MyBlurProps getProps();

protected:
    bool process(frame_container& frames) override;

private:
    class Detail;
    boost::shared_ptr<Detail> mDetail;
};

```

### Implementing the Source File

Place your implementation in `base/src/<YourModule>.cpp`. This file contains the constructor, lifecycle methods, and the `process()` function that executes the actual frame transformation:

```cpp
// base/src/MyBlur.cpp
#include "MyBlur.h"

class MyBlur::Detail {
public:
    MyBlurProps mProps;
    Detail(const MyBlurProps& p) : mProps(p) {}
    void setProps(const MyBlurProps& p) { mProps = p; }
};

MyBlur::MyBlur(MyBlurProps p) : Module(TRANSFORM, "MyBlur", p), mDetail(new Detail(p)) {}

bool MyBlur::init()   { return true; }
bool MyBlur::term()   { return true; }

void MyBlur::setProps(MyBlurProps& p) {
    if (!canQueueProps()) { mDetail->setProps(p); }
    else { Module::addPropsToQueue(p); }
}
MyBlurProps MyBlur::getProps() { return mDetail->mProps; }

bool MyBlur::process(frame_container& frames) {
    // Apply a simple blur using OpenCV or NPP – omitted for brevity
    return true;
}

```

## Step 2 – Register the Module with the Declarative Registry

Registration connects your C++ implementation to the declarative pipeline system. Add your module to [`base/src/declarative/ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp) inside the `ensureBuiltinModulesRegistered()` function:

```cpp
// base/src/declarative/ModuleRegistrations.cpp
// Inside ensureBuiltinModulesRegistered()
if (!registry.hasModule("MyBlur")) {
    registerModule<MyBlur, MyBlurProps>()
        .category(ModuleCategory::Transform)
        .description("Simple Gaussian blur (CPU) with configurable radius")
        .tags("transform", "blur", "cpu")
        .input("input", "RawImage")
        .output("output", "RawImage")
        .intProp("radius", "Blur kernel radius (odd number)", true, 3, 1, 31)
        .dynamicProp("radius", "int", "Runtime‑adjustable blur radius", false, "3")
        .selfManagedOutputPins();   // not needed for static pins but harmless
}

```

The registration macros (`intProp`, `dynamicProp`, etc.) map JavaScript/JSON property names to your C++ `Props` class fields. The `registerModule<>` template instantiates the factory that creates your module when pipelines reference the type name `"MyBlur"`.

## Step 3 – Use the Module in Pipelines

Once registered and rebuilt, your module is accessible by its type name without additional binding code.

### JSON Pipeline Configuration

Reference the module in a JSON pipeline file:

```json
{
  "modules": {
    "blur": {
      "type": "MyBlur",
      "props": { "radius": 5 }
    }
  },
  "connections": [
    { "src": "source", "dst": "blur", "srcPin": "output", "dstPin": "input" },
    { "src": "blur",   "dst": "sink",   "srcPin": "output", "dstPin": "input" }
  ]
}

```

Run the pipeline using the CLI:

```bash
./build/aprapipes_cli run mypipeline.json

```

### Node.js API Integration

Access the module through the native addon:

```js
const ap = require('./aprapipes.node');

const pipeline = ap.createPipeline({
  modules: {
    blur: { type: "MyBlur", props: { radius: 5 } }
  },
  connections: []
});

const blurMod = pipeline.getModule('blur');
console.log('Current radius =', blurMod.getProperty('radius'));
blurMod.setProperty('radius', 9);   // Dynamic change while running

```

## Understanding the Registration Architecture

The declarative system uses a centralized registry that maps type names to factory functions. The flow works as follows:

```text
JSON / JS → ModuleRegistry (looks up type name) → ModuleFactory (creates instance)
          ↑                                             |
          └─ registration metadata (inputs/outputs/props) stored in ModuleInfo

```

The `ensureBuiltinModulesRegistered()` function—called automatically by [`aprapipes_cli.cpp`](https://github.com/apra-labs/aprapipes/blob/main/aprapipes_cli.cpp) and the Node.js [`addon.cpp`](https://github.com/apra-labs/aprapipes/blob/main/addon.cpp)—populates the registry by executing the registration blocks in [`ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/ModuleRegistrations.cpp). When a pipeline requests a module by type name, the factory uses the stored `ModuleInfo` to instantiate the class, bind properties via `applyProperties`, and wire input/output pins.

## Key Files Reference

| File | Role | Example Reference |
|------|------|-------------------|
| `base/include/<YourModule>.h` | Header with `*Props` and module class definitions | [VirtualPTZ.h](https://github.com/apra-labs/aprapipes/blob/main/base/include/VirtualPTZ.h) |
| `base/src/<YourModule>.cpp` | Implementation of processing logic and property handling | [VirtualPTZ.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/src/VirtualPTZ.cpp) |
| [`base/src/declarative/ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp) | Central registration of all built-in modules | [ModuleRegistrations.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp) |
| [`base/tools/aprapipes_cli.cpp`](https://github.com/apra-labs/aprapipes/blob/main/base/tools/aprapipes_cli.cpp) | CLI entry point that triggers registration | [aprapipes_cli.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/tools/aprapipes_cli.cpp) |
| [`base/bindings/node/addon.cpp`](https://github.com/apra-labs/aprapipes/blob/main/base/bindings/node/addon.cpp) | Node.js addon entry point | [addon.cpp](https://github.com/apra-labs/aprapipes/blob/main/base/bindings/node/addon.cpp) |
| [`docs/declarative-pipeline/DEVELOPER_GUIDE.md`](https://github.com/apra-labs/aprapipes/blob/main/docs/declarative-pipeline/DEVELOPER_GUIDE.md) | Comprehensive developer documentation | [DEVELOPER_GUIDE.md](https://github.com/apra-labs/aprapipes/blob/main/docs/declarative-pipeline/DEVELOPER_GUIDE.md) |

## Summary

- **Define the Props class**: Inherit from `ModuleProps` and implement `applyProperties()`, plus optional dynamic property getters/setters for runtime configuration.
- **Implement the Module class**: Inherit from `Module`, implement `init()`, `term()`, and `process()`, and handle property updates via `setProps()` and `canQueueProps()`.
- **Register declaratively**: Add a `registerModule<>` entry in [`base/src/declarative/ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp) specifying inputs, outputs, and property metadata.
- **Rebuild and deploy**: Compile with `cmake --build` and use the module immediately in JSON pipelines or Node.js applications without additional binding code.

## Frequently Asked Questions

### Where should I place the header and source files for a new custom module?

Place the header file in `base/include/<YourModule>.h` and the implementation in `base/src/<YourModule>.cpp`. This follows the existing project structure used by reference modules like `VirtualPTZ` and ensures the build system automatically includes your files when compiling the `base` library.

### How does the declarative registry know about my module's properties?

The registry learns about your properties through the `applyProperties` template method in your `*Props` class and the registration macros in [`ModuleRegistrations.cpp`](https://github.com/apra-labs/aprapipes/blob/main/ModuleRegistrations.cpp). When you call `.intProp()` or `.dynamicProp()` during registration, you map JSON/JavaScript property names to the C++ fields. The `ModuleFactory` uses this metadata to automatically bind values from pipeline configurations to your class instances via `applyProperties()`.

### Can I update module properties while the pipeline is running?

Yes, if you implement dynamic property support. Define `dynamicPropertyNames()`, `getProperty()`, and `setProperty()` in your `Props` class, and use the `.dynamicProp()` macro during registration. At runtime, call `canQueueProps()` to check if updates can be queued, or apply them immediately if the module supports dynamic changes. The Node.js API exposes this through `module.getProperty()` and `module.setProperty()` calls.

### What is the difference between `selfManagedOutputPins()` and standard pin management?

Standard pin management allows the framework to automatically create and connect output pins based on the registration metadata. Calling `selfManagedOutputPins()` indicates that your module manually controls its output pin lifecycle, which is useful for modules that dynamically change their output format based on input or internal state. For most static transforms, you can omit this or include it as a safeguard.