08-writing-modules.md

QPlan’s capabilities expand via ActionModules. This guide covers function/object module patterns, metadata rules, ctx/asynchronous handling, and debugging tips.

1. ActionModule styles

StyleDescriptionExamples
FunctionExport a function and attach metadata via Object.assign().echoModule, sleepModule, mathModule
ObjectProvide { execute(inputs, ctx) { ... } }.fileModule, httpModule, aiModule
// Function example
export const echoModule = Object.assign(
  (inputs: Record<string, any>) => inputs,
  {
    id: "echo",
    description: "Return input as-is",
    usage: `echo msg="hello" -> out`,
    inputs: ["msg"],
  }
);

// Object example
export const fileModule = {
  id: "file",
  description: "Read/write files",
  usage: `file read path="./a.txt" -> txt`,
  inputs: ["op", "path", "data"],
  async execute(inputs, ctx) {
    ...
  }
};

2. Metadata rules

  • id (required): name used in QPlan commands; must be unique.
  • description: concise explanation shown verbatim in prompts.
  • usage: actual QPlan snippet (multi-line allowed).
  • inputs: list of supported parameter names to guide the AI.

Both styles share the same metadata schema, surfaced via registry.list().

3. Handling inputs & ctx

  • If a string argument matches a ctx variable, the executor auto-injects its value. Modules can also call ctx.has/ctx.get.
  • When you need raw JSON, parse it inside the module (see json, http).
  • Throw explicit errors (throw new Error(...)) when params are missing/invalid so the step’s onError policy kicks in.

4. Async work & futures

  • Plain async functions can return Promises; the executor awaits them.
  • To leverage Future/Join, return { __future: Promise }. Example: the future module wraps a setTimeout promise and stores it in ctx.
export const futureModule = {
  id: "future",
  execute(inputs) {
    const p = new Promise(res =>
      setTimeout(() => res(inputs.value), inputs.delay)
    );
    return { __future: p };
  }
};

5. Files & external dependencies

  • When using Node APIs (fs, path, etc.), keep everything under async/await.
  • Be mindful of sandbox/permission policies for network calls (e.g., httpModule uses fetch).

6. Registering & testing

  1. After writing the module, register via registry.register(customModule) or registry.registerAll([...]).
  2. Build quick steps similar to docs/05-examples.md to verify behavior.
  3. Run npm run validate -- - or call runQplan directly for tests.
  4. Confirm metadata includes id/description/usage/inputs so AI prompts pick it up.

7. Best practices

  • Keep modules stateless; inject external services/classes when needed.
  • Provide clear error messages for humans and AI alike.
  • Use console.log/console.error judiciously—avoid noisy output.
  • Reuse helper functions or other modules to prevent duplication.

Follow this guide to write consistent QPlan modules and integrate them cleanly into the registry/AI workflow.