Logic Components
Un Logic Component es una función JavaScript que corre en el servidor, aislada en un sandbox, con un SDK (ctx) para hacer lo que la plataforma permite. Se invoca desde automations (nodo Componente Custom), desde Steppers, desde Visual Components (sdk.runLogic), desde endpoints custom de la Public API, desde tareas programadas, o como verificador de un webhook.
Si lo que querés es UI, lo tuyo es un Visual Component. Un Visual suele tener un Logic de backend.
Crear uno
- Configuración → Constructor de app → Logic Components → Nuevo: label, name (
snake_case, es el identificador para invocarlo), descripción. - En el detalle, el editor con autocompletado del SDK y el código inicial.
- Parameters: los inputs que recibe (texto, número, booleano, picklist, fecha, registro, JSON) y los outputs que devuelve. Se vuelven campos en el builder de automations y en
sdk.runLogic. - Run as: con qué identidad corre (ver abajo). Timeout en milisegundos (hasta 30 000).
- Debugger: ejecutalo con inputs de prueba y mirá salida, logs y duración.
El contrato del handler
export default async function (ctx) {
const account = await ctx.data.getRecord("accounts", ctx.inputs.account_id);
if (!account) return ctx.fail("Account not found", { code: "NOT_FOUND", field: "account_id" });
return ctx.success({ name: account.name });
}Un solo export default, sin import. El sandbox evalúa el archivo como un módulo con un único export. Un export function o export const con nombre al lado del default rompe en tiempo de ejecución con Unexpected token 'export': el editor y el deploy lo aceptan, y falla recién cuando alguien lo usa. Para testear funciones puras, dejalas internas (sin export) y probá el handler entero con un ctx falso.
- No hay
fetch,require,processni acceso a archivos o red: soloctx. Para HTTP externo,ctx.integrations. ctx.success(outputs)yctx.fail(message, { code?, field?, retryable? })son las dos salidas válidas. Unthrowcuenta como fallo.- En una automation antes de guardar, un
ctx.failcancela el guardado y el usuario ve el mensaje.
Contexto de ejecución (Run as)
| Modo | Quién “es” el código |
|---|---|
| Heredar | Lo que decida quien lo invoca (la automation, el endpoint) |
| Usuario que ejecuta (default) | La persona que disparó la acción: sus permisos y su sharing limitan ctx.data |
| Usuario específico | Una cuenta fija (de servicio) |
| Sistema | Sin restricciones de permisos ni sharing |
ctx.user es la identidad efectiva; ctx.caller es quien disparó originalmente. ctx.user.can("read", "accounts") permite preguntar por permisos antes de intentar.
El SDK ctx
| Namespace | Métodos | Notas |
|---|---|---|
ctx.inputs · ctx.record · ctx.user · ctx.caller | — | Los inputs declarados; el registro del contexto si lo hay; las identidades |
ctx.data | getRecord(entity, id) · queryRecords(entity, { filters, limit, sort }) · createRecord(entity, data) · updateRecord(entity, id, data) · deleteRecord(entity, id) · cloneRecord(entity, sourceId, overrides?) | Pasan por el mismo pipeline que la UI (permisos, validación, automations, sharing, historial). Operadores de filtro: eq neq gt gte lt lte contains in not_in is_null is_not_null. cloneRecord no re-mapea relaciones: pasá el nuevo padre en overrides |
ctx.integrations | call(name, { path?, method?, query?, body?, headers? }) → { status, ok, headers, body, durationMs } | La Integration define URL base, auth y headers; la identidad necesita permiso de ejecución sobre ella |
ctx.globals | get(name) · update(name, data) | Global structures |
ctx.pdf | render(templateName, data) · list() | Renderiza un template PDF; con opciones de guardado queda como archivo del registro |
ctx.email | send({ to, subject, html, fromInboxId?, links?, threadId?, attachments? }) · listSenders() · linkThread(threadId, { entity, id }) | Envía por la inbox del workspace; con links la conversación queda vinculada a esos registros; threadId continúa un hilo |
ctx.emailTemplates | get(slug) | Carga un email template |
ctx.files | upload({ name, mime, base64, attachable_kind?, attachable_id?, metadata? }) → { fileId } · list({ attachable_kind, attachable_id, filter? }) · update(fileId, metadata) · remove(fileId) · thumbnail(fileId, variant?) | Adjunta un archivo a un registro y maneja sus adjuntos: listarlos, marcarlos (hidden, public, position, alt), borrarlos y pedir su miniatura. Ver Adjuntos desde un componente |
ctx.components | run(name, inputs?) · list({ kind? }) | Componer Logic Components; listar los disponibles (los de paquetes se resuelven por su prefijo) |
ctx.automations | list({ triggerEvent? }) | Listar automations |
ctx.publicLinks | ensureForComponent(componentName, inputs, { primaryInputKey?, expiresAt? }) → { url, token, linkId, slug } | Crea (o reutiliza) un link público para un Visual publicable |
ctx.crypto | hmacSha256(secret, data) · hashSha256(data) · safeCompare(a, b) · randomBytes(n) · checkTimestampFresh(ts, maxAgeSec) | Para verificar firmas de webhooks y generar tokens |
ctx.dates | nowIso() · parseISO · format · addDays · addMonths · addHours · daysBetween · startOfDay · startOfMonth · endOfMonth · isBefore · isAfter · isSameDay | Fechas sin librerías |
ctx.log | info · warn · error | Se ven en el log de la automation y en el Debugger |
Los nombres de entidad se resuelven en el ámbito del componente: un Logic instalado por el paquete CPQ puede escribir "quote" y Nxar lo traduce a cpq__quote.
Ejemplo: enriquecer una cuenta desde una API externa
- En Integrations, creá
clearbit_enrich(URL base, método, auth). Dale permiso de ejecución al permission set de la identidad con que va a correr. - El handler:
export default async function (ctx) {
const account = await ctx.data.getRecord("accounts", ctx.inputs.account_id);
if (!account?.website) return ctx.fail("Account has no website", { code: "MISSING_WEBSITE" });
const res = await ctx.integrations.call("clearbit_enrich", { body: { domain: account.website } });
if (!res.ok) return ctx.fail(`Provider error ${res.status}`, { code: "PROVIDER_ERROR", retryable: true });
await ctx.data.updateRecord("accounts", account.id, {
industry: res.body.industry,
employee_count: res.body.employees,
});
return ctx.success({ industry: res.body.industry, enriched_at: ctx.dates.nowIso() });
}- Declará el input
account_id(registro, requerido) y los outputs; en la automation, mapeáaccount_id ← {{record.id}}.
Las credenciales viven en la Integration, nunca en el código ni en los inputs.
Adjuntos desde un componente
ctx.files deja que un componente maneje sus propios adjuntos de un registro, separados de la lista de Adjuntos de la Record Page. Es lo que usa el componente Fotos del sitio del paquete Real Estate: las fotos que sube no aparecen entre los adjuntos privados y son las únicas que el sitio público puede mostrar.
export default async function handler(ctx) {
const recordId = ctx.record.id;
const { action, file, fileId, order } = ctx.inputs;
if (action === "upload") {
// Oculto en Adjuntos y publicable en un sitio
await ctx.files.upload({
name: file.name, mime: file.mime, base64: file.base64,
attachable_kind: "record", attachable_id: recordId,
metadata: { hidden: true, public: true },
});
}
if (action === "reorder") {
for (const [i, id] of order.entries()) await ctx.files.update(id, { position: i });
}
if (action === "remove") await ctx.files.remove(fileId);
// Las fotos, en orden, con su miniatura (un Visual Component sólo muestra imágenes data:)
const photos = await ctx.files.list({
attachable_kind: "record", attachable_id: recordId,
filter: { hidden: true, imagesOnly: true },
});
return {
photos: await Promise.all(photos.map(async (p) => ({
id: p.id, name: p.name, alt: p.metadata.alt ?? "",
thumb: await ctx.files.thumbnail(p.id, "w320"),
}))),
};
}| Marca | Qué hace |
|---|---|
hidden | La lista de Adjuntos no lo muestra ni lo cuenta: lo maneja tu componente. |
public | Un sitio público lo puede mostrar, si el visitante puede ver el registro. Ocultar nunca publica: son dos marcas distintas. |
position | El orden dentro de tu componente (list ya devuelve ordenado). |
alt | Texto alternativo de una imagen (hasta 300 caracteres). |
update sólo acepta esas cuatro claves (null borra una); cualquier otra es un error. Los permisos son los de la API de archivos: listar y ver la miniatura piden poder leer el registro; marcar y borrar, poder editarlo (o haber subido el archivo).
Dónde se usa
| Desde | Cómo |
|---|---|
| Automations | Nodo Componente Custom: inputs mapeados, outputs como variables |
| Visual Components | sdk.runLogic("name", inputs, recordId?) |
| Steppers | Entre pantallas, como cualquier nodo |
| Public API | Un Custom Endpoint con target Logic Component (POST /public/v1/custom/<slug>), o como verificador HMAC de un webhook |
| Scheduled jobs | Una tarea programada del workspace, o un schedule cron declarado por un paquete |
| Botones del encabezado | A través de un Visual con presentation.kind = "action" que lo invoca |
Debugging
- Debugger en el detalle: inputs de prueba → salida, logs, duración.
Permission deniedenctx.data: revisá el Run as. Con usuario que ejecuta, esa persona necesita permiso sobre la entidad y acceso al registro.Permission deniedal llamar una integración: la identidad no tiene la casilla Puede ejecutar de esa integración en su permission set.- Timeout: el handler superó su límite (máximo 30 s). Dividí el trabajo o reducí llamadas.
- Unexpected token ‘export’: hay un export con nombre. Ver el contrato.
- Component not found desde
ctx.components.run: el nombre no coincide (los de paquete llevan prefijo, salvo que los invoques desde el mismo paquete).
Encontrar el tuyo entre los del paquete
Con paquetes instalados, la lista de Configuración → Logic Components mezcla los tuyos con decenas de managed. El desplegable Origen, al lado del buscador, la acota a Creados acá, De paquetes o un paquete puntual; la columna Paquete dice de cuál viene cada fila. Mismo filtro en Visual Components, Automations, Steppers, Entidades y App Pages.
Permisos
Crear y editar componentes requiere acceso a Configuración (access_settings); no hay un permiso específico para componentes. Ejecutarlos no requiere permiso propio: se aplican los de la identidad con que corren. Los componentes instalados por un paquete son de solo lectura (se clonan para modificar la copia).