Skip to Content

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

  1. Configuración → Constructor de app → Logic Components → Nuevo: label, name (snake_case, es el identificador para invocarlo), descripción.
  2. En el detalle, el editor con autocompletado del SDK y el código inicial.
  3. 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.
  4. Run as: con qué identidad corre (ver abajo). Timeout en milisegundos (hasta 30 000).
  5. 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, process ni acceso a archivos o red: solo ctx. Para HTTP externo, ctx.integrations.
  • ctx.success(outputs) y ctx.fail(message, { code?, field?, retryable? }) son las dos salidas válidas. Un throw cuenta como fallo.
  • En una automation antes de guardar, un ctx.fail cancela el guardado y el usuario ve el mensaje.

Contexto de ejecución (Run as)

ModoQuién “es” el código
HeredarLo 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íficoUna cuenta fija (de servicio)
SistemaSin 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

NamespaceMétodosNotas
ctx.inputs · ctx.record · ctx.user · ctx.caller—Los inputs declarados; el registro del contexto si lo hay; las identidades
ctx.datagetRecord(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.integrationscall(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.globalsget(name) · update(name, data)Global structures
ctx.pdfrender(templateName, data) · list()Renderiza un template PDF; con opciones de guardado queda como archivo del registro
ctx.emailsend({ 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.emailTemplatesget(slug)Carga un email template
ctx.filesupload({ 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.componentsrun(name, inputs?) · list({ kind? })Componer Logic Components; listar los disponibles (los de paquetes se resuelven por su prefijo)
ctx.automationslist({ triggerEvent? })Listar automations
ctx.publicLinksensureForComponent(componentName, inputs, { primaryInputKey?, expiresAt? }) → { url, token, linkId, slug }Crea (o reutiliza) un link público para un Visual publicable
ctx.cryptohmacSha256(secret, data) · hashSha256(data) · safeCompare(a, b) · randomBytes(n) · checkTimestampFresh(ts, maxAgeSec)Para verificar firmas de webhooks y generar tokens
ctx.datesnowIso() · parseISO · format · addDays · addMonths · addHours · daysBetween · startOfDay · startOfMonth · endOfMonth · isBefore · isAfter · isSameDayFechas sin librerías
ctx.loginfo · warn · errorSe 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

  1. 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.
  2. 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() }); }
  1. 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"), }))), }; }
MarcaQué hace
hiddenLa lista de Adjuntos no lo muestra ni lo cuenta: lo maneja tu componente.
publicUn sitio público lo puede mostrar, si el visitante puede ver el registro. Ocultar nunca publica: son dos marcas distintas.
positionEl orden dentro de tu componente (list ya devuelve ordenado).
altTexto 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

DesdeCómo
AutomationsNodo Componente Custom: inputs mapeados, outputs como variables
Visual Componentssdk.runLogic("name", inputs, recordId?)
SteppersEntre pantallas, como cualquier nodo
Public APIUn Custom Endpoint con target Logic Component (POST /public/v1/custom/<slug>), o como verificador HMAC de un webhook
Scheduled jobsUna tarea programada del workspace, o un schedule cron declarado por un paquete
Botones del encabezadoA 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 denied en ctx.data: revisá el Run as. Con usuario que ejecuta, esa persona necesita permiso sobre la entidad y acceso al registro.
  • Permission denied al 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).

Last updated on