Eventos y el SDK
Los eventos de los widgets y de la pantalla, el editor de código, las acciones predefinidas y el SDK keplin en TypeScript — datos, widgets, navegación, sesión, modales y workflows.
Hay mucha pantalla que se hace sin escribir una línea: conectar un datastore, arrastrar widgets, apuntar un botón a otra pantalla. Pero tarde o temprano aparece el «cuando esto pase, haz aquello» — guardar y volver atrás, recargar una tabla después de un filtro, confirmar antes de eliminar, abrir un modal y usar lo que devolvió.
Para eso sirven los eventos: puntos de la pantalla donde corre código
tuyo, escrito en TypeScript, con un SDK — el objeto keplin — que da
acceso a todo lo que la pantalla tiene.
Dónde están los eventos
En el inspector, la última categoría de un widget (y de la propia pantalla) se llama Eventos. Tiene una fila por evento disponible, y en cada fila:
- un punto a la izquierda: lleno cuando ese evento ya tiene código, vacío cuando no;
- un botón … a la derecha, que abre el editor.

Los nombres de los eventos no se traducen — son los mismos en cualquier
idioma (onClick, onRowClick, onLoad), porque son también los nombres
que aparecen en el código y en los registros del Radar.
Los eventos de la pantalla
Haz clic en una zona vacía del canvas para que el inspector muestre la pantalla. La categoría Eventos tiene tres:
| Evento | Cuándo se dispara | Para qué |
|---|---|---|
onLoad |
Una vez, cuando la pantalla se abre. | Preparar estado, cargar cosas que los datastores no cargan, dar la bienvenida. |
onParamsChange |
Siempre que los parámetros de la ruta cambian — y no en la primera apertura. | Reaccionar a un cambio de registro sin reabrir la pantalla. |
onUnload |
Cuando la pantalla sale. | Limpiar estado, guardar borradores. |

Los eventos de cada widget
Cada tipo de widget declara los suyos. Además del nombre, importa el
payload — los datos que el evento trae consigo, y que el código lee en
keplin.event.
Campos de formulario
| Widget | Eventos | keplin.event |
|---|---|---|
| Caja de texto, Área de texto, Número, Sí/No, Lista, Fecha, Color | onChange |
{ value } |
| Archivo | onChange, onUpload |
onUpload: { file, name } |
Acciones y navegación
| Widget | Eventos | keplin.event |
|---|---|---|
| Botón | onClick |
{} |
| Botón con menú | onClick, onMenuItem |
onMenuItem: { id, label } |
| Enlace | onClick |
{} |
| Exportar | onExport, onDataLoaded |
onExport: { rows, filename, truncated } |
Estructura y contenido
| Widget | Eventos | keplin.event |
|---|---|---|
| Pestañas | onTabChange |
{ tab } |
| Informe | onLoad |
{ report } |
Widgets de datos
| Widget | Eventos | keplin.event |
|---|---|---|
| Tabla | onRowClick, onRowDoubleClick, onSelectionChange, onDataLoaded |
{ row, index } · onSelectionChange: { row, rows } |
| Lista, Tarjetas | onRowClick, onDataLoaded |
{ row, index } |
| Gráfico | onClick |
{ name, seriesName, value, dataIndex } |
| KPI | onClick, onDataLoaded |
{ value, indicatorId } |
Tableros y planificación
| Widget | Eventos | keplin.event |
|---|---|---|
| Kanban | onCardClick, onCardCreate, onCardMoved, onDataLoaded |
{ row } · { column } · { row, from, to, index } |
| Calendario | onEventClick, onDayClick, onRangeSelect, onRangeChange, onDataLoaded |
{ row } · { date } · { start, end } · { start, end, view } |
| Gantt | onBarClick, onEmptyClick, onDataLoaded |
{ row } · { date } |
Procesos
| Widget | Eventos | keplin.event |
|---|---|---|
| Estado del proceso | onDecide |
{ task, outcome } |
| Mis tareas | onOpen, onDecide |
{ task, screenId } |
Nota
Los widgets programados por ti (los que aparecen en la paleta en Custom) declaran sus propios eventos, y aparecen aquí como cualesquiera otros.
El editor de código
El botón … de un evento abre el editor en un modal. El título dice
dónde estás: el id del widget (o el nombre de la pantalla) y el nombre
del evento — w_fic_sav1 · onClick.

| Botón | Qué hace |
|---|---|
| Insertar acción | Escribe por ti el código de una tarea común (a continuación). |
| Quitar handler | Borra el código de este evento. El punto vuelve a vacío. |
| Cancelar | Cierra sin guardar. |
| Guardar | Verifica y guarda. |
El editor tiene sugerencias mientras escribes (Ctrl+Espacio): el
keplin entero está declarado, con los tipos correctos — y, mejor aún,
los ids de los widgets de esta pantalla están ahí dentro. Escribir
keplin.widgets.get(" muestra la lista de los widgets de la pantalla, y un
id que no existe se señala como error antes de guardar.
Atención
Al guardar, el código se compila. Si no es ejecutable, la plataforma lo rechaza — El evento no se guardó: el código no es ejecutable — y el modal queda abierto para corregir. Una pantalla nunca se queda con código roto dentro.
Las acciones predefinidas
Insertar acción abre una lista de las tareas más comunes. Eliges una y el código se escribe al final de lo que ya está, ya con los nombres reales de tu pantalla — el primer datastore de registro, la primera tabla, la primera caja de texto.

| Acción | Qué escribe |
|---|---|
| Guardar datastore | Valida y guarda el registro, con aviso de éxito. |
| Recargar datastore | Vuelve a leer los datos de un datastore. |
| Filtrar datastore (por texto) | Lee el texto de una caja y lo aplica como filtro. |
| Navegar a una pantalla | Salta a otra ruta de la app. |
| Recargar una tabla | Refresca los datos de un widget de tabla. |
| Filtrar tabla por el texto de un campo | El filtro interactivo clásico. |
| Mostrar/ocultar un widget | Alterna la visibilidad de un widget. |
| Confirmar y mostrar toast | Pregunta antes de actuar y avisa al final. |
| Arrancar un workflow | Pone un proceso en marcha sobre el registro actual. |
| Ver y concluir tareas | Lista las tareas de quien está usando y decide una. |
| Mandar una señal a un workflow | Despierta procesos que estaban esperando. |
| Cerrar sesión | Sale de la app. |
El código insertado es un punto de partida: queda tuyo, y es para editarlo. No vuelve a generarse.
El código se guarda formateado: dividido en líneas, indentado y con los espacios correctos, sin cambiar lo que hace. Vale para todos los editores de código de la plataforma, y para el código que los agentes guardan por MCP. Formatear código, en el menú del editor (botón derecho) o con Shift+Alt+F, formatea al momento, y Ctrl+Z lo deshace. Un código con errores de sintaxis no se formatea: queda como está hasta que lo corrijas.
Cómo corre el código
Cada evento es una función asíncrona que recibe una sola cosa: el
keplin. De ahí salen tres consecuencias prácticas:
awaitfunciona en el nivel superior del código. No hace falta envolver nada.returnsale del evento. Es la forma normal de desistir a medias (por ejemplo, cuando una confirmación fue rechazada).- No hay parámetros. El contexto viene dentro del propio
keplin:keplin.eventtrae el payload ykeplin.ctxdice dónde estás (ctx.widgetes el widget que disparó —nullen los eventos de pantalla —,ctx.eventes el nombre del evento yctx.screenla pantalla).
Mientras el código de un botón no termina, el botón muestra tres puntos animados: quien está usando entiende que la app está trabajando. Si el código revienta, la pantalla no se rompe: aparece un aviso y el error queda registrado en el Radar, con la pantalla, el widget y el evento donde ocurrió.
El SDK keplin
Todo lo que el código puede hacer está bajo keplin. Estas son las áreas:
| Área | Para qué |
|---|---|
keplin.event / keplin.ctx |
El payload del evento y el contexto donde está corriendo. |
keplin.widgets |
Hablar con los widgets de la pantalla. |
keplin.data |
Los datastores: leer, escribir, filtrar, guardar. |
keplin.nav |
Navegar y leer los parámetros de la ruta. |
keplin.ui |
Avisos, confirmaciones y modales. |
keplin.state |
Estado compartido entre pantallas. |
keplin.session |
Quién está usando la app, y qué puede hacer. |
keplin.auth |
Login, registro y recuperación de contraseña (pantallas de sistema). |
keplin.i18n |
Frases traducidas, el idioma en uso y el cambio de idioma. |
keplin.storage |
Preferencias guardadas en el dispositivo. |
keplin.api |
Llamar a las APIs de la app directamente. |
keplin.reports |
Abrir y descargar informes. |
keplin.workflow |
Arrancar procesos, listar y concluir tareas. |
Los widgets
keplin.widgets.get("id") devuelve el handle de un widget. Todos los
handles tienen lo mismo básico:
const w = keplin.widgets.get("w_fic_tel1");
w.show(); // mostrar
w.hide(); // ocultar
w.setEnabled(false); // desactivar
w.set("label", "Telemóvel"); // cambiar cualquier propiedad del inspector
w.get("label"); // leer el valor efectivo
w.reset(); // olvidar los cambios hechos en ejecución
Y después cada familia añade lo suyo:
| Familia | Qué añade |
|---|---|
| Campos de formulario | getValue(), setValue(v), validate(), error |
| Widgets de datos (Tabla, Lista, Tarjetas, Gráfico, KPI, Kanban, Calendario, Gantt) | rows, total, refresh(), setFilter(where), setSort(sort) |
| Tabla | selectedRow, selectedRows, clearSelection() |
| KPI | value, values, valueOf(indicadorId) |
| Kanban | columns, moveCard(id, columna, índice?) |
| Calendario | view, start, end, goTo(fecha), setView(vista) |
| Gantt | zoom, setZoom(z) |
| Pestañas | activeTab, tab("id") — y, en la pestaña, activate(), show(), hide(), setEnabled() |
| Etiqueta / Botón / Enlace / Breadcrumb | setText(t) / setLabel(t) |
| Markdown | setContent(md) |
| Página externa | setUrl(url), reload() |
| Exportar | export() |
| Informe | url, download() |
Nota
Las sugerencias del editor ofrecen todos los verbos de todas las
familias, porque el editor no sabe de antemano qué widget es ese id. En
ejecución solo existen los del tipo real — moveCard en un Botón no hace
nada útil.
Los datos
keplin.data.store("nombre") devuelve un datastore de la pantalla por el
nombre (ver
Datastores y datos).
En un datastore de registro:
const conta = keplin.data.store("conta");
conta.get("nome"); // leer un campo
conta.set("estado", "ativo"); // escribir un campo (queda por guardar)
conta.record(); // el registro entero
conta.isDirty(); // ¿hay cambios por guardar?
conta.reset(); // tirar los cambios
const ok = await conta.save(); // valida y guarda; true si guardó
En un datastore de lista:
const contas = keplin.data.store("contas");
contas.rows(); // las filas cargadas
contas.total(); // el total (cuando el servidor lo da)
await contas.reload(); // volver a leer
contas.setWhere({ estado: { eq: "ativo" } }); // filtro extra; null limpia
contas.setSort([{ field: "nome", direction: "ASC" }]);
contas.goToPage(2);
En ambos, status() dice en qué punto está la carga (idle, loading,
ready, error).
reload() devuelve una promesa: con await, las filas nuevas ya están en
rows(). En una lista sin Cargar automáticamente, es el reload() el que
la hace leer.
Navegación
keplin.nav.go("/ficha-de-conta/17"); // ir a una ruta (con parámetros)
keplin.nav.back(); // volver atrás
keplin.nav.params; // los parámetros de la pantalla actual, por nombre
Avisos, confirmaciones y modales
keplin.ui.toast("Gravado.", "success"); // "success" | "error" | "info"
const ok = await keplin.ui.confirm("Apagar o registo?");
if (!ok) return;
La confirmación es un diálogo con el tema de la app — nunca la caja gris del navegador.
Estado, sesión y preferencias
keplin.state.set("filtroContas", "activas"); // vive mientras la pestaña esté abierta
keplin.state.get("filtroContas");
keplin.state.remove("filtroContas");
keplin.session.user; // { id, username, name } — null en pantallas públicas
keplin.session.roles; // los papeles de quien está usando
keplin.session.can("contas.editar"); // ¿tiene esta acción? (Ajustes ▸ Permisos)
await keplin.session.logout();
keplin.storage.set("colunasContas", ["nome", "cidade"]); // queda en el dispositivo
keplin.storage.get("colunasContas");
Consejo
Para decidir qué puede hacer alguien, pregunta keplin.session.can("...")
y no hasRole("gestor"). Las acciones se declaran en Ajustes ▸
Permisos y sobreviven a reorganizaciones de papeles; el nombre de un
papel, no.
Las APIs y los informes
const linhas = await keplin.api.query("contas", { estado: "ativo" }, ["id", "nome"]);
await keplin.api.mutate("criarConta", { nome: "Nova" }, ["id"]);
keplin.reports.open("Contactos da conta", { contaId: 17 });
keplin.reports.download("Contactos da conta", { contaId: 17 }, "xlsx");
En un argumento de enum, pasa el valor tal como está guardado ({ estado: "Em curso" }). Un argumento JSON acepta listas y objetos; en una consulta SQL a SQL
Server, SQLite u Oracle llegan como texto JSON, que se lee con OPENJSON,
json_each o JSON_TABLE.
Los valores pueden ir como los guardan los campos: el texto «12» de un cuadro de
texto sirve en un argumento numérico, un número sirve en un argumento de texto,
«true» sirve en un booleano, y una fecha (Date) va en ISO.
En una consulta, la lista de campos es obligatoria — es ella la que dice qué quieres traer.
Atención
keplin.reports.open abre una pestaña nueva y por eso no puede quedar
detrás de un await: fuera del gesto del usuario, el navegador bloquea
la ventana. Abre primero, haz el resto después.
Workflows
const registo = keplin.data.store("oportunidade").get("id");
await keplin.workflow.start("wf_aprovacao", registo);
const tarefas = await keplin.workflow.tasks();
await keplin.workflow.complete(tarefas[0].id, "aprovar");
const { woken } = await keplin.workflow.signal("documento-recebido", registo);
Frases traducidas
keplin.i18n.t("{n} contas activas", { n: linhas.length });
keplin.i18n.locale;
keplin.i18n.available;
await keplin.i18n.setLocale("en");
Pantallas modales
Una pantalla de Keplin no es modal porque se abrió de cierta manera — es modal porque fue configurada así. La decisión está en el inspector de la pantalla, en la categoría Presentación:

| Opción | Qué hace |
|---|---|
| Modo | Pantalla (una página normal), Modal (centro) o Panel lateral (derecha). El modo Barra (área de widgets) no se abre encima: es el contenido de un área de widgets de la Navegación de la app. |
| Ancho (px) / Alto (px) | El tamaño del modal. El panel lateral usa toda la altura. |
| Botón de cerrar | Muestra la × en la esquina. |
| Clic fuera cierra / Esc cierra | Las dos salidas habituales. Con una lista o un popover abiertos, el primer clic fuera o Esc cierra solo eso. |
| Refrescar la pantalla de atrás al cerrar | Al cerrar, los datastores de la pantalla que lo llamó vuelven a leer. Lo que esté escrito y sin guardar en un formulario de esa pantalla se mantiene. |
Con cambios sin guardar, cerrar con la ×, con Esc o con un clic fuera
pide confirmación primero. Cuentan los campos ligados a un datastore de
registro que la persona editó y aún no guardó; escribir y borrar no cuenta.
Cerrar por código, con keplin.ui.closeModal, no pregunta.
¿Cuál de los dos? El Modal (centro) sirve para fichas simples — un catálogo con descripción y estado, una confirmación. El Panel lateral (derecha) se abre a la derecha a toda la altura y es la elección para fichas con muchos campos, paneles y listas dentro (un cliente con sus direcciones y contactos): la misma ficha en un modal central queda pequeña y apretada, y la lista de atrás sigue visible al lado. El Ancho (px) es el del panel.
La pista de la propia sección lo resume: Se abre POR ENCIMA de la pantalla
que lo llama (Enlace, eventos o keplin.ui.openModal). Sale de la
navegación directa.
Abrir y cerrar por código
const resultado = await keplin.ui.openModal("/nova-conta", { setor: "banca" });
if (resultado) {
keplin.data.store("contas").reload();
}
openModalrecibe la ruta (o el id) de la pantalla y, opcionalmente, los parámetros.- La promesa solo se resuelve cuando el modal cierra, y trae el valor que el modal devolvió.
- Dentro del modal,
keplin.ui.closeModal(valor)cierra y devuelve ese valor. - Los modales se apilan: un modal puede abrir otro. Con varios paneles laterales abiertos, cada panel de abajo queda visible en una franja a la izquierda del panel de arriba; con Clic fuera cierra activado, un clic en esa franja cierra el panel de arriba.
- Pedir la pantalla que ya está arriba del todo, con los mismos parámetros, no abre otra: la llamada devuelve la promesa de la que está abierta. Es lo que impide que un doble clic abra dos fichas iguales.
Nota
keplin.nav.go("/ruta") hacia una pantalla configurada como Modal
(centro) o Panel lateral (derecha) la abre como modal en vez de
navegar. Es a propósito: una pantalla modal no tiene dirección propia en
la navegación.
Recetas
Guardar y volver (el onClick del botón Guardar de la Ficha de
Conta):
const ok = await keplin.data.store("conta").save();
if (ok) {
keplin.ui.toast("Conta guardada");
keplin.nav.go("/contas");
}
Abrir la ficha de la fila clicada (onRowClick de una Tabla):
keplin.nav.go(`/ficha-de-conta/${keplin.event.row["id"]}`);
Filtrar una tabla por una caja de texto (onChange de la caja —
sustituye los ids por los de tu pantalla):
const texto = keplin.widgets.get("w_pesquisa").getValue();
keplin.widgets.get("w_cta_tab1").setFilter(texto ? { nome: { contains: texto } } : null);
Confirmar antes de una acción destructiva (onClick de un botón):
if (!(await keplin.ui.confirm("Apagar esta conta?"))) return;
Ocultar un botón a quien no puede (onLoad de la pantalla):
if (!keplin.session.can("contas.eliminar")) {
keplin.widgets.get("w_apagar").hide();
}
¿Por qué no…?
- ¿Por qué no me deja guardar el evento? El código no compila. El mensaje es El evento no se guardó: el código no es ejecutable — corrige y guarda.
- ¿Por qué
keplin.widgets.get("...")da error? El id no existe en esta pantalla. Confírmalo en la parte superior del inspector, con el widget seleccionado; y recuerda que cada dispositivo es un árbol propio (ver Layouts y diseño por dispositivo). - ¿Por qué el
onParamsChangeno se disparó al abrir? Es a propósito: solo se dispara en cambios. Para el arranque, usa elonLoad. - ¿Por qué el modal no devuelve nada? O la pantalla de destino no existe, o quien está usando no tiene permiso para abrirla — en los dos casos la promesa se resuelve sin valor. Confirma la ruta y los permisos.
- ¿Por qué no se abre la ventana del informe? Pusiste el
opendespués de unawait. Abre primero. - ¿Por qué mi evento parece no correr? Mira el Radar: los errores del código de los eventos quedan ahí, con pantalla, widget y evento — y el Radar te lleva directamente al editor de ese evento.