diff --git a/extensions/http-functions/README.md b/extensions/http-functions/README.md
new file mode 100644
index 00000000..f1434e51
--- /dev/null
+++ b/extensions/http-functions/README.md
@@ -0,0 +1,340 @@
+# DynamiaTools HTTP Functions Extension
+
+## Overview
+
+The DynamiaTools HTTP Functions Extension provides a declarative, versioned function runtime
+exposed over HTTP. It lets applications define, register, and execute reusable business
+capabilities as versioned functions, without coupling callers to a specific implementation of
+"how to call WhatsApp" / "how to generate this PDF" / "how to call that third-party API".
+
+Instead of hardcoding a REST client for every external integration, you configure a
+`DynamiaHttpFunction` once (method, URL, body template, headers) and call it by name from Java
+code or straight over HTTP. Credentials, endpoints and payload shapes can change without touching
+application code or redeploying.
+
+It acts as an internal capability bus for Dynamia-based applications, and provides:
+
+- Versioned function contracts (`name` + `functionVersion`)
+- HTTP-based invocation (`POST /api/dynamia/fx/{functionName}`) and direct Java invocation
+ (`DynamiaFunctions.call(...)`)
+- Parameter validation and type coercion before execution
+- JSON and binary responses (images, PDFs, CSV, etc.)
+- Deterministic version resolution (highest `ACTIVE` version by default)
+- A back office UI to register functions and test them without leaving the browser
+
+------------------------------------------------------------------------
+
+## Getting Started
+
+### 1. Add the dependency
+
+```xml
+
+ tools.dynamia.modules
+ tools.dynamia.modules.functions.core
+ 26.6.0
+
+
+
+
+ tools.dynamia.modules
+ tools.dynamia.modules.functions.ui
+ 26.6.0
+
+```
+
+No extra configuration is required: `DynamiaHttpFunctionsServiceImpl` and the REST controller are
+auto-registered as long as the module is on the classpath (standard DynamiaTools `@Service` /
+component scanning).
+
+### 2. Register a function
+
+You can create it through the back office UI (`Http Functions` page, once the `ui` module is on
+the classpath), or programmatically:
+
+```java
+DynamiaHttpFunction function = new DynamiaHttpFunction();
+function.setName("WhatsApp.sendMessage");
+function.setFunctionVersion(1);
+function.setMethod(HttpMethod.POST);
+function.setUrl("https://api.whatsapp.example.com/v1/messages");
+function.setContentType("application/json");
+function.setBodyTemplate("{\"to\":\"${number}\",\"text\":\"${message}\"}");
+function.setHeaders("Authorization: Bearer ${apiToken}");
+function.setStatus(FunctionStatus.ACTIVE);
+
+DynamiaHttpFunctionParameter number = new DynamiaHttpFunctionParameter();
+number.setName("number");
+number.setRequired(true);
+function.addParameter(number);
+
+DynamiaHttpFunctionParameter message = new DynamiaHttpFunctionParameter();
+message.setName("message");
+message.setRequired(true);
+function.addParameter(message);
+
+crudService.create(function);
+```
+
+### 3. Call it
+
+From Java:
+
+```java
+FunctionResult result = DynamiaFunctions.call("WhatsApp.sendMessage",
+ Map.of("number", "123456789", "message", "Hello"));
+
+if (result.isSuccess()) {
+ Map data = result.toJson();
+ // or, mapped straight into a DTO:
+ // SendMessageResponse dto = result.toJson(SendMessageResponse.class);
+}
+```
+
+Or over HTTP:
+
+```http
+POST /api/dynamia/fx/WhatsApp.sendMessage
+Content-Type: application/json
+
+{ "params": { "number": "123456789", "message": "Hello" } }
+```
+
+That's it — no client class, no manual `RestTemplate`/`RestClient` wiring, no hardcoded URL in
+your service code.
+
+------------------------------------------------------------------------
+
+## Core Concepts
+
+### 1. `DynamiaHttpFunction`
+
+Represents a versioned function definition. Each function includes:
+
+| Field | Description |
+|-------------------|--------------------------------------------------------------------------------------------|
+| `name` | Function identifier, e.g. `WhatsApp.sendMessage`. Free-form, but a `Namespace.action` convention is recommended. |
+| `functionVersion` | Integer, starts at 1. |
+| `method` | HTTP method used for the outbound call (`HttpMethod`). |
+| `url` | Target URL. Supports `${param}` placeholders. |
+| `contentType` | Content type sent with the request body. Defaults to `application/json`. |
+| `bodyTemplate` | Request body template. Supports `${param}` placeholders. |
+| `headers` | Optional, one `Header-Name: value` per line. Supports `${param}` placeholders — the usual way to inject API keys/tokens. |
+| `status` | `DRAFT`, `ACTIVE`, `INACTIVE`, `DELETED`. Only `ACTIVE` functions can be called. |
+| `metadata` | Optional free-form JSON, informative/extensible only — not used by the execution engine. |
+| `parameters` | List of `DynamiaHttpFunctionParameter` (see below). |
+
+`url`, `bodyTemplate` and `headers` are rendered with the call parameters before the request is
+issued, using `${paramName}` placeholders (e.g. `{"to":"${number}","text":"${message}"}`).
+
+Constraint: `(name, functionVersion)` is unique per account, enforced both by a database index and
+by `DynamiaHttpFunctionValidator`. A function can only reach `ACTIVE` status once it has a `url`.
+
+### 2. `DynamiaHttpFunctionParameter`
+
+Declares one input parameter accepted by a function call:
+
+| Field | Description |
+|----------------|--------------------------------------------------------------------------|
+| `name` | Parameter name, matched against the `params` map at call time. |
+| `type` | `STRING` (default), `NUMBER`, `BOOLEAN`, `DATE` (`yyyy-MM-dd`), `DATETIME` (`yyyy-MM-dd HH:mm:ss`). |
+| `required` | When `true` and the caller doesn't provide a value (and there's no `defaultValue`), the call fails with `400`. |
+| `defaultValue` | Used when the caller omits the parameter. |
+| `position` | Display order in the back office UI. |
+
+### 3. Versioning Model
+
+Versioning is explicit and manual in the MVP.
+
+Rules:
+
+- Versions start at 1
+- A new version must be strictly greater than the current maximum for that `name`
+- No duplicate `(name, version)` combinations
+- By default, the highest **`ACTIVE`** version is executed
+- Specific versions can be requested explicitly, regardless of status — useful for testing a
+ `DRAFT` version before promoting it to `ACTIVE`
+
+```java
+DynamiaFunctions.call("WhatsApp.sendMessage", params); // highest ACTIVE version
+DynamiaFunctions.call("WhatsApp.sendMessage", 2, params); // version 2 explicitly
+```
+
+### 4. Parameter Validation & Type Coercion
+
+Before invoking the function, declared parameters are validated against the call's `params` map:
+
+- Missing values fall back to `defaultValue` when declared
+- Still-missing **required** parameters raise a validation error → HTTP `400`
+- Present values are coerced to the declared `type` (e.g. a `NUMBER` parameter accepts a numeric
+ string and is parsed into a `BigDecimal`); a value that can't be coerced also raises `400`
+
+------------------------------------------------------------------------
+
+## HTTP API
+
+### Base Endpoint
+
+```
+POST /api/dynamia/fx/{functionName}
+```
+
+### Optional Version Selection
+
+- Header: `X-Dynamia-Version: 2`
+- Or query parameter: `?v=2`
+
+If not specified, the latest `ACTIVE` version is used.
+
+### Request
+
+```http
+POST /api/dynamia/fx/WhatsApp.sendMessage
+Content-Type: application/json
+
+{ "params": { "number": "123456789", "message": "Hello" } }
+```
+
+### Response
+
+JSON response:
+
+```json
+{ "success": true, "data": { ... } }
+```
+
+Binary response — when the target endpoint returns an image/PDF/CSV/etc., the raw bytes are
+streamed back with the matching `Content-Type` (e.g. `image/png`), no JSON envelope, no base64
+wrapping.
+
+Error response (validation, not-found, inactive, execution error):
+
+```json
+{ "success": false, "error": "Parameter [number] is required" }
+```
+
+### Status Codes
+
+| Code | Meaning |
+|------|-------------------------------------------------------------|
+| 200 | Success |
+| 400 | Validation error (missing/invalid parameter) |
+| 401 | Authentication required *(delegated to the app's security layer, see below)* |
+| 403 | Authorization failure *(delegated to the app's security layer, see below)* |
+| 404 | Function or version not found, or function not `ACTIVE` |
+| 500 | Internal execution error (target endpoint failed, network error, etc.) |
+
+------------------------------------------------------------------------
+
+## Calling Functions from Java
+
+Functions are resolved through `DynamiaHttpFunctionsService` (registry + execution engine). The
+static facade `DynamiaFunctions` is the recommended entry point for application code:
+
+```java
+// Highest ACTIVE version
+FunctionResult result = DynamiaFunctions.call("WhatsApp.sendMessage",
+ Map.of("number", "123456789", "message", "Hello"));
+
+// Specific version
+FunctionResult v2 = DynamiaFunctions.call("WhatsApp.sendMessage", 2,
+ Map.of("number", "123456789", "message", "Hello"));
+
+// Auto-register a DRAFT placeholder instead of throwing FunctionNotFoundException
+// when the function hasn't been configured yet — handy while wiring up new
+// integrations, so calling code doesn't need a hard dependency on setup order.
+FunctionResult draft = DynamiaFunctions.call("Invoicing.generatePdf",
+ Map.of("orderId", "123"), true);
+
+// Non-blocking (runs on a virtual thread)
+DynamiaFunctions.callAsync("WhatsApp.sendMessage", Map.of("number", "123", "message", "Hello"))
+ .thenAccept(r -> log.info("sent: " + r.isSuccess()));
+```
+
+### Reading the result
+
+`FunctionResult` wraps either structured data or binary content:
+
+```java
+FunctionResult result = DynamiaFunctions.call("WhatsApp.sendMessage", params);
+
+result.isSuccess(); // true/false
+result.isBinary(); // true when the payload is raw bytes (image/PDF/CSV/...)
+result.isJson(); // true when getData() is a Map/List
+
+Map data = result.toJson(); // parsed as a Map
+SendMessageResponse dto = result.toJson(SendMessageResponse.class); // parsed into a DTO
+byte[] file = result.getBinaryData(); // when isBinary() == true
+```
+
+### Error handling
+
+Calls can throw:
+
+- `FunctionNotFoundException` — no function registered for that `(name, version)`
+- `FunctionInactiveException` — function exists but isn't `ACTIVE`
+- `ValidationError` (`tools.dynamia.domain`) — missing/invalid parameter
+- `FunctionExecutionException` — the outbound HTTP call failed (network error, non-2xx response,
+ etc.); wraps the original cause
+
+The REST controller maps these to `404`, `400` and `500` respectively (see [Status
+Codes](#status-codes)). When calling from Java, catch what's relevant to your use case, or let it
+bubble up if the caller should fail loudly.
+
+------------------------------------------------------------------------
+
+## UI Module
+
+The `ui` submodule (`tools.dynamia.modules.functions.ui`, ZK-based) adds a back office CRUD for
+`DynamiaHttpFunction` / `DynamiaHttpFunctionParameter`:
+
+- **Navigation**: a "Http Functions" page group is contributed to the existing `saas` module (see
+ `DynamiaHttpFunctionsModuleProvider`), listing all registered functions.
+- **View descriptors** (`META-INF/descriptors/`): form/table/crud for the function itself, plus a
+ nested `crudview` for its parameters and a form/table pair for `DynamiaHttpFunctionParameter`.
+- **`TestHttpFunctionAction`**: a crud action ("Test") that opens a dialog to edit call parameters
+ as JSON and immediately invokes the selected function through `DynamiaHttpFunctionsService`,
+ showing the resulting `FunctionResult` (or the validation/execution error) without leaving the
+ browser — the fastest way to verify a function definition before wiring it into application
+ code.
+
+------------------------------------------------------------------------
+
+## Design Principles
+
+- Deterministic version resolution
+- Immutable version contracts (a version, once created, isn't meant to be edited — cut a new one)
+- Clear separation between metadata and implementation
+- HTTP-native semantics
+- Extensible without modifying core controllers
+
+------------------------------------------------------------------------
+
+## Example Use Cases
+
+- Send WhatsApp/SMS/email notifications through a third-party provider
+- Generate PDF reports or optimize images via an internal microservice
+- Export data files (CSV, XLSX)
+- Integrate third-party services whose credentials/endpoints vary per environment or tenant
+- Trigger internal business processes exposed as internal HTTP endpoints
+
+------------------------------------------------------------------------
+
+## Known Limitations / Roadmap
+
+- **401/403 enforcement is not done at the function level** — securing who can call
+ `/api/dynamia/fx/{functionName}` is currently delegated entirely to the application's security
+ layer (e.g. Spring Security rules on the base path).
+- `interfaceName` / `methodName` fields exist on `DynamiaHttpFunction` reserved for a future
+ dynamic Java interface proxy (call a function through a regular interface method instead of
+ `DynamiaFunctions.call(...)`), **not implemented yet**.
+- Not yet implemented: deprecation metadata, automatic version suggestions, execution
+ metrics/observability, response caching, script-based execution sandbox.
+
+------------------------------------------------------------------------
+
+## Philosophy
+
+This extension provides a lightweight, versioned function runtime that enables controlled
+evolution of business capabilities while maintaining contract stability. It is designed to support
+long-term extensibility without introducing unnecessary architectural complexity.
diff --git a/extensions/http-functions/sources/core/pom.xml b/extensions/http-functions/sources/core/pom.xml
new file mode 100644
index 00000000..28bd7c6a
--- /dev/null
+++ b/extensions/http-functions/sources/core/pom.xml
@@ -0,0 +1,54 @@
+
+ 4.0.0
+
+ tools.dynamia.modules
+ tools.dynamia.modules.parent
+ 26.6.0
+ ../../../pom.xml
+
+ DynamiaModules - Http Functions Core
+ tools.dynamia.modules.functions.core
+
+ Core module for Http Functions, contains the main classes and interfaces to create and handle http functions, a
+ way to create functions that can be called from Java like normal code and return a response with the result of
+ the function execution.
+
+
+
+
+
+ tools.dynamia.modules
+ tools.dynamia.modules.saas.jpa
+ 26.6.0
+
+
+ tools.dynamia
+ tools.dynamia.web
+ 26.6.0
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.20.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.20.0
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaFunctions.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaFunctions.java
new file mode 100644
index 00000000..47f5e9ee
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaFunctions.java
@@ -0,0 +1,153 @@
+package tools.dynamia.modules.functions;
+
+import tools.dynamia.integration.Containers;
+import tools.dynamia.integration.scheduling.SchedulerUtil;
+import tools.dynamia.modules.functions.services.DynamiaHttpFunctionsService;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Static entry point to call {@link tools.dynamia.modules.functions.domain.DynamiaHttpFunction}s from
+ * regular Java code, as if they were normal method calls, without coupling the caller to a specific
+ * implementation. By default, the highest active version of a function is invoked, but a specific
+ * version can be requested explicitly.
+ *
+ * Example:
+ *
+ *
+ * @author Mario A. Serrano Leones
+ */
+public class DynamiaFunctions {
+
+ private DynamiaFunctions() {
+ }
+
+ /**
+ * Calls the highest active version of a function.
+ *
+ * @param name the function name (e.g. {@code WhatsApp.sendMessage})
+ * @param params the call parameters
+ * @return the execution result
+ */
+ public static FunctionResult call(String name, Map params) {
+ return service().call(name, params);
+ }
+
+ /**
+ * Calls a specific version of a function.
+ *
+ * @param name the function name
+ * @param version the requested version
+ * @param params the call parameters
+ * @return the execution result
+ */
+ public static FunctionResult call(String name, int version, Map params) {
+ return service().call(name, version, params);
+ }
+
+ /**
+ * Calls the highest active version of a function, auto-registering it as a {@code DRAFT} placeholder
+ * when it does not exist yet, so integration code doesn't fail hard while the function is pending
+ * configuration (url, method, body template, etc.) in the back office.
+ *
+ * @param name the function name
+ * @param params the call parameters
+ * @param autoCreate when {@code true}, a missing function is auto-registered as {@code DRAFT} instead
+ * of throwing {@link FunctionNotFoundException}
+ * @return the execution result
+ */
+ public static FunctionResult call(String name, Map params, boolean autoCreate) {
+ return service().call(name, params, autoCreate);
+ }
+
+ /**
+ * Calls a specific version of a function, auto-registering it as a {@code DRAFT} placeholder when it
+ * does not exist yet. See {@link #call(String, Map, boolean)}.
+ *
+ * @param name the function name
+ * @param version the requested version
+ * @param params the call parameters
+ * @param autoCreate when {@code true}, a missing function is auto-registered as {@code DRAFT} instead
+ * of throwing {@link FunctionNotFoundException}
+ * @return the execution result
+ */
+ public static FunctionResult call(String name, int version, Map params, boolean autoCreate) {
+ return service().call(name, version, params, autoCreate);
+ }
+
+ /**
+ * Calls the highest active version of a function asynchronously, on a Virtual Thread (see
+ * {@link SchedulerUtil#runWithResult(java.util.function.Supplier)}), without blocking the caller.
+ *
+ * @param name the function name
+ * @param params the call parameters
+ * @return a future completed with the execution result, or completed exceptionally if the call fails
+ *
+ * Example:
+ *
+ */
+ public static CompletableFuture callAsync(String name, Map params) {
+ return SchedulerUtil.runWithResult(() -> call(name, params));
+ }
+
+ /**
+ * Calls a specific version of a function asynchronously, on a Virtual Thread. See
+ * {@link #callAsync(String, Map)}.
+ *
+ * @param name the function name
+ * @param version the requested version
+ * @param params the call parameters
+ * @return a future completed with the execution result, or completed exceptionally if the call fails
+ */
+ public static CompletableFuture callAsync(String name, int version, Map params) {
+ return SchedulerUtil.runWithResult(() -> call(name, version, params));
+ }
+
+ /**
+ * Calls the highest active version of a function asynchronously, auto-registering it as a
+ * {@code DRAFT} placeholder when it does not exist yet. See {@link #callAsync(String, Map)} and
+ * {@link #call(String, Map, boolean)}.
+ *
+ * @param name the function name
+ * @param params the call parameters
+ * @param autoCreate when {@code true}, a missing function is auto-registered as {@code DRAFT} instead
+ * of failing the future with {@link FunctionNotFoundException}
+ * @return a future completed with the execution result, or completed exceptionally if the call fails
+ */
+ public static CompletableFuture callAsync(String name, Map params, boolean autoCreate) {
+ return SchedulerUtil.runWithResult(() -> call(name, params, autoCreate));
+ }
+
+ /**
+ * Calls a specific version of a function asynchronously, auto-registering it as a {@code DRAFT}
+ * placeholder when it does not exist yet. See {@link #callAsync(String, Map, boolean)}.
+ *
+ * @param name the function name
+ * @param version the requested version
+ * @param params the call parameters
+ * @param autoCreate when {@code true}, a missing function is auto-registered as {@code DRAFT} instead
+ * of failing the future with {@link FunctionNotFoundException}
+ * @return a future completed with the execution result, or completed exceptionally if the call fails
+ */
+ public static CompletableFuture callAsync(String name, int version, Map params, boolean autoCreate) {
+ return SchedulerUtil.runWithResult(() -> call(name, version, params, autoCreate));
+ }
+
+ private static DynamiaHttpFunctionsService service() {
+ DynamiaHttpFunctionsService service = Containers.get().findObject(DynamiaHttpFunctionsService.class);
+ if (service == null) {
+ throw new IllegalStateException("No " + DynamiaHttpFunctionsService.class.getName() + " implementation available");
+ }
+ return service;
+ }
+}
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaHttpFunctionValidator.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaHttpFunctionValidator.java
new file mode 100644
index 00000000..45dbd1aa
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/DynamiaHttpFunctionValidator.java
@@ -0,0 +1,59 @@
+package tools.dynamia.modules.functions;
+
+import tools.dynamia.domain.InstallValidator;
+import tools.dynamia.domain.ValidationError;
+import tools.dynamia.domain.Validator;
+import tools.dynamia.domain.ValidatorUtil;
+import tools.dynamia.domain.query.QueryParameters;
+import tools.dynamia.domain.services.CrudService;
+import tools.dynamia.domain.util.DomainUtils;
+import tools.dynamia.modules.functions.domain.DynamiaHttpFunction;
+import tools.dynamia.modules.functions.domain.enums.FunctionStatus;
+
+/**
+ * Enforces the versioning rules described in the extension's README for
+ * {@link DynamiaHttpFunction}: versions start at 1, {@code (name, functionVersion)} is unique, and a
+ * new version of an existing function must be strictly greater than the current maximum version. The
+ * {@code url} is only required once the function is {@code ACTIVE}, so it can be auto-registered as a
+ * {@code DRAFT} placeholder pending configuration (see {@code DynamiaHttpFunctionsService#call} auto
+ * registration overloads).
+ *
+ * @author Mario A. Serrano Leones
+ */
+@InstallValidator
+public class DynamiaHttpFunctionValidator implements Validator {
+
+ @Override
+ public void validate(DynamiaHttpFunction function) throws ValidationError {
+ ValidatorUtil.validateEmpty(function.getName(), "Function name is required");
+ if (function.getStatus() == FunctionStatus.ACTIVE) {
+ ValidatorUtil.validateEmpty(function.getUrl(), "Function url is required");
+ }
+
+ if (function.getFunctionVersion() < 1) {
+ throw new ValidationError("Function version must start at 1");
+ }
+
+ CrudService crudService = DomainUtils.lookupCrudService();
+
+ if (function.getId() == null) {
+ QueryParameters params = QueryParameters.with("name", function.getName())
+ .add("functionVersion", function.getFunctionVersion());
+ if (crudService.count(DynamiaHttpFunction.class, params) > 0) {
+ throw new ValidationError("Function [%s] version [%s] already exists", function.getName(), function.getFunctionVersion());
+ }
+
+ Integer maxVersion = findMaxVersion(crudService, function.getName());
+ if (maxVersion != null && function.getFunctionVersion() <= maxVersion) {
+ throw new ValidationError("New version of function [%s] must be greater than the current maximum version [%s]",
+ function.getName(), maxVersion);
+ }
+ }
+ }
+
+ private Integer findMaxVersion(CrudService crudService, String name) {
+ var versions = crudService.find(DynamiaHttpFunction.class,
+ QueryParameters.with("name", name).orderBy("functionVersion", false));
+ return versions.isEmpty() ? null : versions.get(0).getFunctionVersion();
+ }
+}
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionExecutionException.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionExecutionException.java
new file mode 100644
index 00000000..4c3933c5
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionExecutionException.java
@@ -0,0 +1,15 @@
+package tools.dynamia.modules.functions;
+
+/**
+ * Thrown when a {@link tools.dynamia.modules.functions.domain.DynamiaHttpFunction} fails during
+ * execution (e.g. the target endpoint is unreachable or returns an unexpected error). Callers should
+ * map this exception to an HTTP 500 response.
+ *
+ * @author Mario A. Serrano Leones
+ */
+public class FunctionExecutionException extends RuntimeException {
+
+ public FunctionExecutionException(String name, int version, Throwable cause) {
+ super("Error executing function [" + name + "] version [" + version + "]: " + cause.getMessage(), cause);
+ }
+}
\ No newline at end of file
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionInactiveException.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionInactiveException.java
new file mode 100644
index 00000000..492f3fa0
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionInactiveException.java
@@ -0,0 +1,18 @@
+package tools.dynamia.modules.functions;
+
+/**
+ * Thrown when a requested {@link tools.dynamia.modules.functions.domain.DynamiaHttpFunction} (or a
+ * specific version of it) exists but is not {@code ACTIVE} (e.g. it is still {@code DRAFT},
+ * {@code INACTIVE} or {@code DELETED}). Callers should map this exception to an HTTP 404 response,
+ * distinguishing it from {@link FunctionNotFoundException} for troubleshooting purposes.
+ *
+ * @author Mario A. Serrano Leones
+ */
+public class FunctionInactiveException extends RuntimeException {
+
+ public FunctionInactiveException(String name, Integer version) {
+ super(version != null
+ ? "Function [" + name + "] version [" + version + "] is not active"
+ : "Function [" + name + "] is not active");
+ }
+}
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionNotFoundException.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionNotFoundException.java
new file mode 100644
index 00000000..31a6353c
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionNotFoundException.java
@@ -0,0 +1,17 @@
+package tools.dynamia.modules.functions;
+
+/**
+ * Thrown when a requested {@link tools.dynamia.modules.functions.domain.DynamiaHttpFunction} (or a
+ * specific version of it) does not exist at all. Callers should map this exception to an HTTP 404
+ * response, distinguishing it from {@link FunctionInactiveException} for troubleshooting purposes.
+ *
+ * @author Mario A. Serrano Leones
+ */
+public class FunctionNotFoundException extends RuntimeException {
+
+ public FunctionNotFoundException(String name, Integer version) {
+ super(version != null
+ ? "Function [" + name + "] version [" + version + "] not found"
+ : "Function [" + name + "] not found");
+ }
+}
\ No newline at end of file
diff --git a/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionResult.java b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionResult.java
new file mode 100644
index 00000000..25ff295b
--- /dev/null
+++ b/extensions/http-functions/sources/core/src/main/java/tools/dynamia/modules/functions/FunctionResult.java
@@ -0,0 +1,140 @@
+package tools.dynamia.modules.functions;
+
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents the outcome of a {@link tools.dynamia.modules.functions.domain.DynamiaHttpFunction} execution. A result is either a structured
+ * JSON-like payload ({@link #getData()}) or raw binary content ({@link #getBinaryData()}) with an
+ * associated content type, matching the response model described in the extension's README.
+ *
+ * @author Mario A. Serrano Leones
+ */
+public class FunctionResult {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final boolean success;
+ private final Object data;
+ private final byte[] binaryData;
+ private final String contentType;
+ private final String errorMessage;
+
+ private FunctionResult(boolean success, Object data, byte[] binaryData, String contentType, String errorMessage) {
+ this.success = success;
+ this.data = data;
+ this.binaryData = binaryData;
+ this.contentType = contentType;
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * Creates a successful result with a structured (JSON-serializable) payload.
+ *
+ * @param data the response payload
+ * @return a successful {@link FunctionResult}
+ */
+ public static FunctionResult success(Object data) {
+ return new FunctionResult(true, data, null, "application/json", null);
+ }
+
+ /**
+ * Creates a successful result carrying raw binary content (image, PDF, CSV, etc.).
+ *
+ * @param binaryData the raw bytes to stream back to the caller
+ * @param contentType the content type of the binary payload
+ * @return a successful {@link FunctionResult}
+ */
+ public static FunctionResult binary(byte[] binaryData, String contentType) {
+ return new FunctionResult(true, null, binaryData, contentType, null);
+ }
+
+ /**
+ * Creates a failed result with an error message.
+ *
+ * @param errorMessage the error description
+ * @return a failed {@link FunctionResult}
+ */
+ public static FunctionResult error(String errorMessage) {
+ return new FunctionResult(false, null, null, null, errorMessage);
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public boolean isBinary() {
+ return binaryData != null;
+ }
+
+ /**
+ * Indicates whether {@link #getData()} holds a structured JSON payload (object or array) that can be
+ * read with {@link #toJson()} or {@link #toJson(Class)}.
+ *
+ * @return {@code true} when the result is not binary and carries a JSON object/array payload
+ */
+ public boolean isJson() {
+ return !isBinary() && (data instanceof Map || data instanceof List);
+ }
+
+ /**
+ * Returns {@link #getData()} as a {@code Map}, parsing it first when it was kept as a raw JSON
+ * string (e.g. because the target endpoint returned malformed JSON).
+ *
+ * @return the payload as a {@link Map}, or an empty map when there is no data
+ */
+ @SuppressWarnings("unchecked")
+ public Map toJson() {
+ return convert(Map.class);
+ }
+
+ /**
+ * Converts {@link #getData()} into the given DTO class using Jackson, so callers don't have to deal
+ * with raw {@code Map}/{@code String} payloads.
+ *
+ * @param type the target class to parse/convert the payload into
+ * @param the target type
+ * @return the payload converted to {@code type}, or {@code null} when there is no data
+ *
+ * Example:
+ *