Cada herramienta, en un solo lugar
Esta página es la referencia práctica para cada herramienta de la barra lateral de tu entorno Archestack. Cada sección cubre qué hace la herramienta, cuándo recurrirías a ella, los pasos para los flujos de trabajo más comunes y las trampas que atrapan a los usuarios nuevos. La sección final (APIs invocables desde scripts) documenta la superficie exacta que pueden invocar los scripts.
Schema Designer
Un editor visual del esquema de base de datos, tablas, columnas, tipos, relaciones, índices, que guarda automáticamente en un documento JSON. El esquema es la fuente de la verdad para tu aplicación; ninguna otra herramienta ve una columna hasta que la añades aquí y despliegas.
Flujo de trabajo habitual: en la barra de herramientas, haz clic en Add Table (botón con icono). Se
abre un diálogo titulado "Create New Table"; escribe un nombre (el texto de ayuda recuerda
"Lowercase with underscores recommended"), haz clic en Create. La tabla aparece en el
lienzo con una clave primaria SERIAL id autogenerada. Selecciona la tabla, el
panel de la derecha se abre en la pestaña Columns. Añade columnas desde el cuadro
"Add column" con borde discontinuo en la parte inferior (campo de nombre, desplegable Type,
haz clic en Add Column). Haz clic en cada columna para expandirla y ajustar los
conmutadores (PK, NULL, UQ), Length y
Default value.
Tipos de columna disponibles en el desplegable Type: SERIAL, BIGSERIAL, INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, VARCHAR, CHAR, TEXT, BOOLEAN, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, UUID, JSON, JSONB, BYTEA, INET, CIDR, MACADDR, MONEY, INTERVAL, más tipos geométricos y de array. La longitud solo es relevante para VARCHAR y CHAR.
Las claves foráneas viven en la pestaña Relations del panel derecho (no "Relationships"). Desplázate hasta "Add relationship": elige la columna FK en la tabla actual, el tipo de relación (One-to-One / One-to-Many / Many-to-Many), la tabla referenciada, la columna referenciada, y haz clic en Add Relationship. Cada relación tiene desplegables On Delete y On Update (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).
Guardado automático: el indicador en la parte inferior izquierda parpadea entre "Auto saving…" y "Saved". También hay un botón Save en la barra de herramientas (solo se activa cuando hay cambios pendientes) para guardados explícitos.
Otros botones de la barra de herramientas: Add Group (contenedores visuales para tablas relacionadas, panel de propiedades titulado "Group"), Add Text (anotaciones libres, panel de propiedades titulado "Text Annotation"), controles de zoom, filtro de paquetes, búsqueda de tablas.
Trampas: renombrar una columna produce un plan destructivo (eliminar + añadir),
edita el SQL en la pestaña Generated SQL del despliegue para usar RENAME COLUMN si
quieres preservar los datos. Añadir una columna NOT NULL a una tabla no vacía sin un valor por
defecto falla, hazla nullable, rellena datos y luego altera. El botón Deploy de la
barra de herramientas te lleva a una página de configuración de despliegue, no directamente a
un despliegue.
Database Deployments
Lista cada despliegue con su estado (Draft / Executing / Succeeded / Failed) y ejecuta los
nuevos. Ruta: /database-deployments/overview.
Botones de la parte superior derecha: Refresh (recarga la lista), New Deployment (crea un nuevo borrador).
Desde Schema Designer: el botón con icono de cohete verde Deploy en
la barra de herramientas de Schema Designer te lleva a /database-deployments/configure/new,
una página de configuración de despliegue nueva.
Página de configuración de despliegue: dos pestañas.
- Configuration - editores de scripts pre/post-despliegue (PostgreSQL) con un botón Test Run cada uno. El pre-script se ejecuta antes de los cambios de esquema; el post-script, después. Útil para rellenos, reconstrucciones de índices o limpieza de zonas de staging. Más un conmutador Force Deploy (Allow Data Loss) para operaciones destructivas.
- Generated SQL - muestra la migración que ha producido la plataforma. El SQL es editable. Un botón Regenerate (icono de refresco) vuelve a ejecutar el diff si has hecho cambios de esquema desde que abriste esta página.
Botones superiores: Back, Save (guarda la configuración como Draft), Deploy (ejecuta el despliegue inmediatamente). Deploy es lo último que pulsas.
Trampas: los despliegues no se envuelven automáticamente en una transacción,
un fallo a mitad deja la base de datos en un estado parcial. Para migraciones arriesgadas,
envuelve tus sentencias en BEGIN; … COMMIT; en el pre-script. El conmutador
Force Deploy se salta las comprobaciones de seguridad; úsalo deliberadamente.
Business Entities
Vistas curadas construidas sobre una tabla maestra más joins de tablas relacionadas. Las páginas se vinculan a Business Entities, nunca a tablas crudas. Consulta Conceptos -> Business Entities para el porqué.
Flujo de trabajo habitual: haz clic en Create. El editor a página completa se abre con las pestañas Visual, JSON, Events. Establece Entity Name, Master Table (Autocomplete) y Label Column (la columna cuyo valor representa un registro en los selectores). Guarda, la BE autogenera una lista de columnas nativas a partir de la tabla maestra.
Añadir joins: desplázate a la tarjeta Join Configurator y haz clic en Add Join. Cada join se abre como un Accordion con: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete agrupado por Database Tables / Third Party), From Column, To Column, desplegables opcionales Type Cast, y un selector de columnas para qué columnas del destino exponer.
Agregaciones: en un join, activa Aggregate Mode. Por cada columna elegida, eliges una Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Número de contactos abiertos de esta empresa" es el ejemplo canónico.
Run Preview: el editor tiene un botón Run Preview (con un icono de reproducción) que ejecuta la configuración completa de join y muestra filas reales en una rejilla de datos. Si una columna unida vuelve vacía, la columna no estaba marcada en el selector de columnas del join (o la relación está mal configurada).
Múltiples BEs por tabla maestra: nada te impide tener
customer (sales view) y customer (support view) sobre la misma tabla
maestra. Páginas diferentes, columnas apropiadas para cada audiencia, la misma fila subyacente.
Trampas: las columnas unidas y agregadas son de solo lectura. Para editar la etiqueta unida, navega al registro fuente. Las columnas agregadas se recalculan en cada consulta, bien para cientos de filas, lento con millones.
Page Editor
Configura la interfaz de tiempo de ejecución vinculando una Page a una Business Entity. La plataforma autogenera secciones basadas en las columnas de la BE; ajustas el diseño a partir de ahí. No hay un "selector de plantillas" separado, cada página parte de la misma línea base generada y la personalizas.
Flujo de trabajo habitual: haz clic en Create. Rellena Page Name,
Page Route (por ejemplo /companies), elige la Business Entity
(Autocomplete). La página se abre en el editor con las pestañas Visual, Overview,
Create, Entities, Events, JSON. Ajusta el diseño
autogenerado, luego cambia el conmutador Published de la cabecera superior a ON.
Tipos de widget de campo (el Select Type de cada campo): Text,
Textarea, Number, Date, Select, Checkbox, Email. Usa Select para campos de clave
foránea y establece el autocompletado Entity del campo a la BE referenciada para que
los usuarios elijan por etiqueta.
Pestañas en el formulario de detalle: haz clic en Add Tab en la
pestaña Visual. Dentro de una pestaña puedes añadir una sección RelatedGrid vinculada
a otra BE filtrada por la PK del registro actual. Ejemplo clásico: una página Company con
pestañas "Contacts" y "Deals", cada una filtrada por
company_id = id de la empresa actual.
Botones de acción en el inspector Header del formulario de detalle. Haz clic en Add en la sección Actions. Cada acción tiene Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info) y Steps (una lista ordenada de: Save Record, Delete Record, Navigate, Business Events, Custom). Para ejecutar un Business Event desde un botón, establece el tipo de paso a Business Events y elige el evento por nombre.
Publicación: el conmutador Published en la cabecera superior es el único control de publicación. ON = la página aparece bajo Published Pages en la barra lateral y los usuarios finales que naveguen a su ruta la ven. OFF = borrador (solo tú en el editor ves tus cambios).
Trampas: las columnas autogeneradas en una Page nueva reflejan la BE, si la BE cambia (añades un join), la Page no recogerá automáticamente las nuevas columnas. Recarga la página en el editor o añade la columna manualmente a la sección. Actualmente no hay un tipo de paso de acción "Generate PDF", para conectar un PDF a un botón de página, expón el PDF mediante un Script Module que llame al endpoint REST y usa un paso Custom (o llama al endpoint directamente desde una Frontend Template).
Business Events
Reglas que se disparan ante cambios de datos (o según un horario, o manualmente) y ejecutan acciones cuando se cumplen las condiciones. El mecanismo "cuando ocurra X, haz Y" de la plataforma. Consulta Conceptos -> Business Events para el modelo.
Flujo de trabajo habitual: haz clic en Create. Establece Rule Name, activa Enabled. Elige Business Entity. Marca uno o más Triggers: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Construye el árbol de condiciones (grupos AND/OR). Añade una o más acciones en el FlowCanvas. Guarda.
Tipos de acción (RuleActionKind):
- Execute Script - ejecuta un script C#. La acción más flexible; recurre a ella siempre que quieras establecer un campo, hacer un cálculo, llamar a una API, cualquier cosa personalizada. El script recibe
Entity(múdalo en triggers Before* para cambiar la escritura en curso),OldEntity,Log,Db,Modules. - Validate - un script que devuelve
bool.truesignifica que la validación falla y el guardado se bloquea con el mensaje de error configurado. Opcionalmente resalta columnas específicas vía errorColumns. - Block Operation - detiene la operación de golpe con un mensaje configurado. Sin script.
- Create Entity - inserta un registro en otra BE. Los valores de los campos admiten expresiones de plantilla como
{{ Entity.column_name }}y{{ now() }}. - Update Entity - actualiza registros en otra BE que coincidan con un filtro de condiciones. Las mismas expresiones de plantilla.
- Delete Entity - elimina registros que coincidan con una condición. Se niega a dispararse sin una condición (seguridad).
- Send Email / Send Webhook / Publish Event - definidos en el esquema como mejoras futuras; actualmente se registran y se omiten.
Patrón "establecer un campo": no hay una acción discreta Set Field.
La forma de establecer un campo es un Execute Script sobre un trigger Before* que
muta Entity. Ejemplo: Entity.title = ((string)Entity.title)?.Trim();. La
plataforma persiste el Entity modificado como parte de la escritura en curso, sin consulta
extra, sin riesgo de recursión.
Run Simulation: la pestaña Simulate del editor de trigger tiene un
selector de registros y un botón Run Simulation. La plataforma ejecuta tus
condiciones y acciones contra el registro elegido sin persistir cambios: las escrituras se
comprueban y se registran, pero nunca se ejecutan, incluidas las de un script con Db
y Files, así que nada llega a la base de datos ni a los archivos del workspace y
tampoco se dispara ningún otro Business Event. El correo y las notificaciones no se envían. Todo
lo demás que hace un script por su cuenta es real: las llamadas HTTP, System.IO y
todo lo que hace un paquete NuGet, como una conexión a otra base de
datos. El panel de salida muestra qué condiciones
coincidieron y qué habría hecho cada acción. Úsalo para detectar configuraciones erróneas antes
de que los datos en vivo los encuentren.
Expresiones de plantilla en la configuración de acciones usan llaves
{{ … }}. Los tokens reales: {{ Entity.column }}, {{ OldEntity.column }},
{{ now() }} / {{ getdate() }}, {{ today() }},
{{ guid() }} / {{ newid() }}, {{ year() }},
{{ month() }}, {{ day() }}, {{ timestamp() }},
agregados como {{ SUM(column) }} dentro de contextos de join, y aritmética
post-sustitución como {{ Entity.quantity * Entity.price }}. No
hay token {{ user.email }}.
Operadores de condición: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).
Trampas: un trigger que muta el mismo registro que observa se volverá a
disparar a sí mismo si usas Update Entity; usa en su lugar Before Update +
Execute Script + Entity.field = …. Dos triggers sobre el mismo evento se
disparan en orden de prioridad. Los eventos desactivados siguen apareciendo en la lista, el
conmutador Enabled es independiente de la configuración del trigger.
Script Modules
Scripts C# reutilizables compilados en tiempo de ejecución por Roslyn. Aceptan parámetros,
consultan la base de datos mediante el helper Db, devuelven un valor. Invocables
desde Business Events, Scheduled Events y directamente desde el front-end.
Flujo de trabajo habitual: haz clic en Create. El editor se abre con las pestañas Edit,
Parameters, Test, JSON. Da nombre al módulo (PascalCase, por ejemplo
RecalcOpenRevenue). En la pestaña Parameters, haz clic en Add por
cada parámetro (Name, Type, Required, Default Value). Opciones de tipo: string, int, decimal, double,
bool, DateTime. Vuelve a la pestaña Edit, escribe C#. IntelliSense está activado.
Panel de prueba: la pestaña Test muestra tus entradas de parámetros a la izquierda y un botón Run (icono PlayArrow). Haz clic en Run; el lado derecho muestra un indicador de éxito o error, el log de salida y el valor de retorno (formateado como JSON).
El helper Db, consulta la referencia completa en APIs invocables desde scripts más abajo.
Llamar desde un Business Event: añade una acción Execute Script cuyo cuerpo llame al módulo:
// The module is a method; its name and parameters are checked when the script compiles
await Modules.RecalcCompanyOpenDealValue(company_id: (int)Entity.company_id);
// Calling it by name still works
await Modules.CallAsync("RecalcCompanyOpenDealValue", new Dictionary<string, object?> {
["company_id"] = Entity.company_id
}); Llamar desde un Scheduled Event: crea un evento con el trigger
On Schedule, establece el cron, añade una acción Execute Script con el mismo
cuerpo Modules.CallAsync.
Registro: cada invocación escribe una entrada en Event Logs con el tiempo de ejecución, parámetros y resultado (o error).
Trampas: siempre await las llamadas a Db, olvidarlo compila pero
devuelve un Task. Los retornos se pasan como objeto (no los serializa el framework como JSON);
para consumo desde el front-end, devuelve objetos anónimos con campos primitivos. La sugerencia
al carácter de confirmación está intencionadamente desactivada en el editor; usa Tab para aceptar.
Shared Code
Código que necesitan varios scripts o páginas, guardado en archivos propios. El nombre de un
archivo es su ruta, como Sales/OrderSummary.cs o components/StatusChip.tsx,
y su extensión indica qué es: .cs es C# que puede usar cualquier script, mientras que
.tsx, .ts, .jsx y .js son módulos que importan
las páginas, las frontend templates y los bloques Code de los layouts. Los archivos compartidos
pertenecen a paquetes como cualquier otro artefacto.
Flujo de trabajo habitual: en la barra lateral, bajo Logic, abre Shared Code y haz clic en New file. Escribe la ruta; el editor inicia el archivo con un ejemplo de su tipo. Escribe el código y haz clic en Save.
Los archivos de C# son C# normal: namespaces, records, clases y helpers estáticos.
Todos los scripts (script modules, business events, scheduled events, data scripts de PDF y
pruebas) pueden usar sus tipos públicos. Un tipo sin namespace está disponible por su nombre; uno
dentro de un namespace necesita un using en el script, o un global using
en cualquier archivo compartido, que entonces se aplica a todos los scripts. Un archivo compartido
ve el modelo de datos tipado (Order,
WorkspaceDb), pero no los globales de un script, así que un helper que necesita
Db o Log lo recibe como argumento.
// Shared code: Sales/OrderSummary.cs, ordinary C# every script can use
namespace Sales;
/// <summary>One order as the invoices and the dashboard report it.</summary>
public record OrderSummary(int Id, string Number, decimal Total);
public static class Vat
{
public static decimal Add(decimal amount, decimal rate = 0.21m) =>
Math.Round(amount * (1 + rate), 2);
} // A script module using it
using Sales;
var open = await Db.Order.Where(o => o.Status == "Open").ToListAsync();
return open.Select(o => new OrderSummary(o.Id, o.Number, Vat.Add(o.Total ?? 0))).ToList(); Un cambio guardado llega a cada script en su siguiente ejecución. El editor comprueba un archivo compartido junto con los demás archivos compartidos, y Go to Definition sobre un tipo compartido en un script abre el archivo que lo declara. Cuando el código compartido no compila, los scripts que lo usan fallan e indican el archivo que está mal; los scripts que no lo usan siguen ejecutándose. Un cambio en una tabla que rompería un archivo compartido se notifica, junto con los scripts que rompe, antes de que se ejecute el despliegue.
Los módulos se importan desde @/ seguido de su ruta sin la
extensión: components/StatusChip.tsx es
import { StatusChip } from "@/components/StatusChip". Una página también
puede tener archivos propios junto a sus vistas, que se añaden con + encima de su editor
de código y se importan como ./OrderRow. Un módulo ve los mismos globales que el
código de páginas y exporta lo que usan los demás. Una vista con importaciones puede terminar con
la exportación por defecto de su componente en lugar de con return. El código sin
import ni export se ejecuta exactamente como siempre.
// Shared code: components/StatusChip.tsx, a module pages import
export function StatusChip({ status }: { status: string }) {
const color = status === "Open" ? "warning" : status === "Paid" ? "success" : "default";
return <Chip size="small" color={color} label={status} />;
} // A page's overview, with a file of its own, OrderRow.tsx, beside it
import { StatusChip } from "@/components/StatusChip";
import { OrderRow } from "./OrderRow";
export default function Orders() {
const [rows, setRows] = useState([]);
useEffect(() => {
backend.getBusinessEntityData("Order").then((r) => setRows(r.rows.items));
}, []);
return (
<Stack spacing={1}>
{rows.map((row) => (
<OrderRow key={row.id} row={row}>
<StatusChip status={row.orders_status} />
</OrderRow>
))}
</Stack>
);
}
Los paquetes que el código puede importar son los que ya incluye la aplicación, y una importación
es el mismo objeto que contienen los globales de la página: react,
@mui/material, @mui/x-date-pickers, dayjs,
lucide-react, three, @react-three/fiber y
@react-three/drei. Ningún otro paquete está disponible.
Publicación: la aplicación publicada lee los módulos compartidos tal como los fijó la release actual, así que una edición llega a los usuarios finales con la siguiente publicación, igual que una página. Un visitante que no ha iniciado sesión recibe solo los módulos que importan las páginas públicas y los layouts públicos.
Trampas: la ruta es la identidad de un archivo en todos los espacios de trabajo, y
las importaciones la nombran, así que renombrar un archivo rompe lo que lo importa hasta que esas
importaciones se actualicen. Las rutas usan letras, dígitos, puntos, guiones bajos y guiones. El
namespace Archestack está reservado. Un alias en un global using
(global using Money = decimal;) no pasa a los scripts.
NuGet Packages
Bibliotecas de NuGet para los scripts de C# del workspace: un driver para otra base de datos, un
lector de CSV o de Excel, un cliente para un servicio. Todos los scripts pueden usarlas (script
modules, scripts de business events, scheduled events, data scripts de PDF, scripts de prueba y
comprobaciones de código), y también los archivos .cs compartidos. El editor de
scripts y VS Code autocompletan sus tipos. Un workspace que no añade ninguno ejecuta sus scripts
exactamente como antes.
Flujo de trabajo habitual: en la barra lateral, bajo Logic, abre NuGet Packages y haz clic en Add NuGet package. Búscalo, o escribe su id exacto cuando en la fuente no se puede buscar, y elige una versión exacta: la última estable viene preseleccionada, e Include prerelease versions muestra las demás. En Belongs to, elige a qué pertenece: uno de los paquetes del workspace, o Workspace only. Add lo descarga primero en el servidor, junto con sus dependencias. Si eso falla, el diálogo explica por qué y no se guarda nada; si funciona, muestra los namespaces que aporta.
En un paquete: un paquete NuGet que pertenece a un paquete viaja con él, en su
exportación y en lo que despliega un pipeline, e instalar un paquete enumera los paquetes NuGet
que añade o cuya versión sube antes de que cambie nada. En un repositorio están en
packages/<package>/nuget.json, que solo se escribe cuando el paquete declara
alguno, y VS Code comprueba el archivo mientras escribes. Un push que cambia el archivo los
descarga primero, y se rechaza, sin cambiar nada, cuando no se pueden restaurar.
{
"packages": [
{ "id": "CsvHelper", "version": "33.0.1" },
{ "id": "Dapper", "version": "2.1.35" },
{ "id": "MySqlConnector", "version": "2.4.0" }
]
} Usar uno: un paquete NuGet aporta tipos, nunca un global, y nada se importa por
ti. Escribe un using para su namespace, que a menudo no es el id: la columna
Namespaces de la página muestra los namespaces de cada paquete NuGet como líneas
using listas para copiar. Devuelve valores simples desde un script, no los objetos
propios de la biblioteca.
// NuGet package: CsvHelper. Reads a price list an admin uploaded on the Files page.
using System.Globalization;
using System.IO;
using CsvHelper;
var text = await Files.ReadTextAsync("imports/prices.csv");
var csv = new CsvReader(new StringReader(text), CultureInfo.InvariantCulture);
var rows = new List<object>();
try
{
csv.Read();
csv.ReadHeader();
while (csv.Read())
rows.Add(new { Sku = csv.GetField("sku"), Price = csv.GetField<decimal>("price") });
}
finally
{
csv.Dispose();
}
// Plain rows, not CsvHelper objects.
return rows; // NuGet packages: MySqlConnector and Dapper. A script module that lists the open invoices in
// the old ERP's MySQL database. The connection string is a file an admin uploads on the Files
// page of each environment: a password written in a script travels to git, package exports
// and every environment.
using Dapper;
using MySqlConnector;
var connectionString = await Files.ReadTextAsync("config/erp-connection.txt");
var connection = new MySqlConnection(connectionString.Trim());
try
{
await connection.OpenAsync();
var invoices = await connection.QueryAsync(
"SELECT number, customer_number, amount FROM invoices WHERE paid = 0 ORDER BY number");
// Plain values, not driver objects.
return invoices
.Select(i => new { Number = (string)i.number, Customer = (string)i.customer_number, Amount = (decimal)i.amount })
.ToList();
}
finally
{
// A top-level using var does not parse in a script, so the connection is closed here.
await connection.DisposeAsync();
} Versiones: una declaración indica una única versión exacta, como
2.1.35, nunca un rango. Cuando varios paquetes piden versiones distintas del mismo
paquete NuGet, todos los scripts del workspace usan la más alta. La columna In use muestra
qué versión es y quién la pidió.
Qué copia se ejecuta: un workspace carga su propia copia de cada paquete NuGet,
separada de la de cualquier otro workspace del servidor, también cuando el servidor lleva la misma
biblioteca para sí mismo. El propio .NET y las bibliotecas que los scripts ya tienen, como
System.Text.Json, el logging, EF Core y Npgsql, siguen siendo la copia del servidor.
Un paquete NuGet que pide una versión más nueva de una de ellas recibe una advertencia: se ejecuta
con la copia del servidor, y un método que solo tiene la versión más nueva falla cuando se llama.
Restauraciones: el primer script que necesita los paquetes NuGet en un servidor hace que ese servidor los descargue. Espera hasta cinco segundos, y si la descarga tarda más, se le pide que vuelva a ejecutarse en un momento. Tras un cambio, el workspace restaura el nuevo conjunto. Hasta que eso funciona, los scripts conservan el último conjunto que se restauró, así que una restauración fallida nunca quita paquetes NuGet que funcionaban. El aviso de la página muestra el estado, con los errores de una restauración fallida explicados. Tras un fallo, el servidor vuelve a intentarlo la primera vez que un script necesita los paquetes NuGet después de la hora que indica el aviso, o en el acto con Restore now. Resolved enumera todos los paquetes NuGet que usan los scripts, dependencias incluidas.
Simulaciones y pruebas: una simulación (la pestaña Test,
Run Simulation) solo retiene lo que pasa por Archestack: las escrituras con
Db y Files se simulan y no se guardan, y el correo y las notificaciones
no se envían. Una ejecución de pruebas hace sus escrituras con Db en su sandbox y
retiene lo demás de la misma manera. Todo lo demás que hace el script por su cuenta es real: las
llamadas HTTP, System.IO y todo lo que hace un paquete NuGet, como una conexión a otra
base de datos, así que una escritura mediante un driver se produce en cada ejecución. Mientras
pruebas un script así, apúntalo a una base de datos de pruebas.
Trampas:
- Los paquetes NuGet necesitan el acceso a datos tipado, igual que el C# compartido. En un servidor que lo desactiva (
Scripting:TypedData:Enabled), la página lo indica y no permite añadir ninguno. #r "nuget: Dapper, 2.1.35", la forma en que otros ejecutores de scripts cargan uno, no funciona aquí. Añádelo en la página.- Una biblioteca nativa que aporta un paquete NuGet se carga una vez por proceso del servidor, la comparten los workspaces que usan el mismo archivo y sigue cargada hasta que el servidor se reinicia.
- Los paquetes NuGet Workspace only no se exportan con un paquete, no están en el repositorio y los pipelines no los despliegan. Asigna un paquete NuGet que necesita una aplicación al paquete de esa aplicación.
- Un
using varde nivel superior es un error de sintaxis en un script, así que cierra tú mismo la conexión, en unfinally, como hace el ejemplo. - Un valor leído de
Entityesdynamic, y un método de extensión comoQueryAsyncde Dapper no puede recibirlo como argumento: el script no compila (CS1973). Haz antes un cast:(string)Entity.customers_number. - Se rechazan los paquetes NuGet que son herramientas, plantillas o MSBuild SDKs, y también uno que no tiene nada contra lo que los scripts puedan compilar en .NET 8, o uno que extiende el propio driver de PostgreSQL del servidor o EF Core. Los ids llamados
OmniCoreoArchestack, o que empiezan por uno de ellos seguido de un punto, son de la plataforma.
Para operadores: los paquetes NuGet están activados por defecto y siempre
desactivados en el modo de prueba (trial). Los ajustes están bajo Scripting:NuGet, o
como variables de entorno Scripting__NuGet__Enabled y similares. Cada servidor
descarga por su cuenta, así que cada uno necesita llegar a nuget.org o a los feeds configurados
aquí, a través del proxy de https_proxy, http_proxy y
no_proxy cuando lo hay. Da las mismas fuentes a todos los servidores de una misma
instalación.
| Ajuste | Valor por defecto | Qué hace |
|---|---|---|
Enabled | true | Si se desactiva, no se descarga nada y los scripts no cargan ninguno. La página no puede añadir uno; las declaraciones que trae una importación o un push se conservan sin usarse. |
Sources | nuget.org | De dónde vienen los paquetes NuGet, separados por ;: feeds por su índice de servicio v3 (la URL que termina en index.json), o carpetas absolutas, de las que se copia y en las que se confía: los límites de descarga solo se aplican a los feeds. nuget.org solo se usa cuando ni este ajuste ni Feeds indican una fuente. |
Feeds:<name>:Url, Username, Password | Ninguno | Un feed con nombre, para uno que necesita credenciales. La URL nunca lleva un nombre de usuario ni una contraseña. |
Feeds:<name>:Patterns | Ninguno | Los ids que sirve este feed, separados por ; (Contoso.*). Cuando algún feed tiene patrones, un id viene del feed cuyo patrón coincide con él, y un feed sin patrones sirve todo lo demás. |
Feeds:<name>:AllowInsecureConnections | false | Deja pasar un feed http:// sin cifrar. |
Directory | Una carpeta en los datos locales de la aplicación | Dónde guarda este servidor los paquetes NuGet que ha descargado: una carpeta por servidor en su disco local, nunca un volumen que compartan los servidores. En un contenedor, dale un volumen propio de ese servidor, o cada contenedor nuevo los vuelve a descargar. |
MaxPackageBytes, MaxExtractedBytes | 100 MB, 1 GB | El paquete NuGet más grande que descarga una restauración, y lo máximo que puede ocupar un paquete NuGet al descomprimirse. |
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages | 500 MB, 2 GB, 150 | Lo máximo que una restauración descarga, descomprime y cuenta en paquetes NuGet, todo junto. |
MaxCacheBytes | 10 GB | Por encima de este tamaño la carpeta no admite nuevas restauraciones, y se eliminan los paquetes NuGet que este servidor no está usando, empezando por los usados hace más tiempo. |
MinFreeDiskBytes | 1 GB | Con menos espacio libre en disco que este, una restauración no empieza, y se detiene una descarga que dejaría menos. |
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds | 300, 120, 5000 | Cuánto puede durar una restauración, cuánto espera la adición de un paquete NuGet a que termine su descarga, y cuánto espera un script a la descarga de este servidor antes de que se le pida volver a ejecutarse. |
AllowNativeAssets | true | Si se desactiva, se rechaza un paquete NuGet que lleva bibliotecas nativas, y nunca se carga ninguna. |
AuditRefuseLevel | Vacío | low, moderate, high o critical: se rechaza un paquete NuGet con una vulnerabilidad conocida de ese nivel o superior. Vacío no rechaza ninguno, y las vulnerabilidades aparecen como advertencias; cualquier otro valor hace fallar toda restauración que añada un paquete NuGet. |
CertificateRevocationMode | online | online u offline: cómo se comprueba la revocación de los certificados de firma de los paquetes NuGet. |
Frontend Templates
Fragmentos de UI reutilizables escritos en TSX que el editor de páginas puede insertar en una página. Útil cuando las plantillas configuradas no son suficientes, gráficos a medida, diseños inusuales, integraciones de widgets de terceros.
Flujo de trabajo habitual: haz clic en Create, da nombre a la plantilla, escribe un componente
TSX en el editor. El componente recibe props: record (la fila actual cuando va
embebida en un formulario de detalle), refresh (volver a obtener los datos), y
unos cuantos helpers. Guarda. Referencia la plantilla desde un campo del Page Editor.
Empaquetable: las plantillas fluyen por el sistema de Package como cualquier otro objeto de configuración, se incluyen en cascada cuando una página que las usa se añade a un paquete.
Trampas: el TSX no puede importar paquetes npm arbitrarios: importa
código compartido desde @/ y los pocos paquetes que incluye la aplicación.
PDF Templates
HTML + plantilla Scriban + un pequeño script C# de datos que renderiza a un PDF (Chromium headless). Cada plantilla es invocable por nombre desde un script vía REST o directamente desde un botón en una Page (con un pequeño adaptador, ver más abajo).
Flujo de trabajo habitual: haz clic en + Create. El editor se abre con campos de texto Name y Description en la parte superior, cinco pestañas en el lado izquierdo (HTML Template, Data Script, Settings, Params, JSON) y un panel PDF Preview siempre visible a la derecha. Las pestañas Data Script y HTML Template abren cada una un editor Monaco en la mitad izquierda; la vista previa se vuelve a renderizar cuando guardas (botón verde Update arriba a la derecha).
Declara parámetros en la pestaña Params mediante el botón + Add (Name, Type, Required, Default Value). La etiqueta de la pestaña se actualiza a Params (n) con el recuento. Formato de página / orientación / márgenes / escala / cabecera / pie viven en la pestaña Settings.
Forma del data script (consulta el tutorial de PDF Invoices para uno real):
var deal = await Db.GetAsync("deal", deal_id);
var lines = await Db.From("deal_line")
.Where("deal_id", "=", deal_id).ToListAsync();
return new {
InvoiceNumber = $"INV-{deal.id:D6}",
Lines = lines.Select(l => new { l.description, l.quantity })
}; Forma de la plantilla HTML (Scriban, similar a Liquid/Handlebars):
<h1>{{ InvoiceNumber }}</h1>
<table>
{{ for line in Lines }}
<tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
{{ end }}
</table> Codificación HTML: cada valor {{ }} se
codifica como HTML, así que un valor de registro que contenga <b> se
imprime como el texto <b>. html.raw es la exclusión explícita,
solo para HTML de confianza: {{ notes | html.raw }}. No lo
uses nunca con valores que los usuarios finales puedan escribir, porque eso mete su marcado en
el documento renderizado. Solo la plantilla puede desactivar la codificación, los datos no.
html.escape y html.newline_to_br siguen funcionando
(newline_to_br escapa su texto pero conserva las etiquetas <br />).
Panel de vista previa: siempre visible en el lado derecho del editor. Se vuelve a renderizar cuando guardas (botón verde Update). Incluye una columna de miniaturas para PDFs multipágina y una barra de herramientas de visor integrada (zoom, rotar, descargar, imprimir). Los botones con icono en la parte superior derecha del editor conmutan la visibilidad del panel de vista previa y la edición a pantalla completa.
Llamarla desde fuera del editor:
POST /api/v1/pdf-templates/{id}/preview- renderiza por ID, devuelveapplication/pdf. Usado por el panel de vista previa del editor.POST /api/v1/pdf-templates/generate/{name}- renderiza por nombre, devuelveapplication/pdf.POST /api/v1/pdf-templates/generate/{name}/base64- igual pero devuelve{ "data": "<base64>" }.
Pasa los valores de los parámetros en el cuerpo JSON. La autenticación es el token Bearer estándar.
Limitación de la prueba: tanto el endpoint generate como el
endpoint {id}/preview (usado por el panel de vista previa del editor)
devuelven 403 en modo de prueba, el renderizado de PDF está completamente restringido. Puedes
seguir creando la plantilla (data script, HTML, ajustes, parámetros) y guardarla; simplemente
no verás un PDF renderizado hasta que estés en un entorno de pago.
Nota sobre el renderizado invocable desde scripts: el contexto de scripting C#
sí expone un global Pdf, así que un Script Module renderiza una plantilla
almacenada directamente con await Pdf.GenerateAsync("invoice", ...) o
Pdf.GenerateBase64Async(...). No hace falta ninguna ida y vuelta con
HttpClient al endpoint generate. (El autocompletado del editor de
scripts sigue anunciando Pdf.RenderAsync y un objeto Templates;
ninguno de los dos nombres existe en tiempo de ejecución.)
Trampas: Scriban escapa HTML por defecto. Los saltos de página están guiados
por CSS, page-break-before: always en una sección. Las fuentes disponibles en el
servidor de renderizado se limitan a las fuentes del sistema más las familias Noto y
Liberation, embebe la tuya vía base64 @font-face si necesitas una fuente de marca.
Imágenes, fuentes y hojas de estilo: el renderer solo carga recursos desde
direcciones http(s) públicas, además de URLs data: y
blob:. Las direcciones internas y de red privada (localhost,
10.x, 192.168.x, nombres de servicio de contenedores, etc.) están
bloqueadas, así que una imagen servida por un servicio interno no aparecerá en el PDF. Usa una
URL pública o una URL data:.
Scheduled Events
El mismo sistema de Event Trigger que Business Events, con el trigger On Schedule marcado. Úsalo para recálculos nocturnos, resúmenes semanales, limpiezas periódicas, informes mensuales.
Flujo de trabajo habitual: crea un Business Event con el trigger On Schedule
(en lugar de, o además de, los triggers de cambio de datos). Configura la expresión cron.
Añade una acción Execute Script cuyo cuerpo invoque un Script Module mediante
Modules.CallAsync(...).
Expresiones cron: 6 o 7 campos al estilo Quartz, consulta Conceptos -> Scheduled Events para patrones comunes. Las horas son hora del servidor (UTC en la pila de producción).
Trampas: un trabajo largo que se pasa de su intervalo cron no se duplica, Quartz no disparará una segunda instancia del mismo trabajo a la vez, el segundo tick se omite. Las ejecuciones fallidas no reintentan automáticamente; integra el reintento en el script.
Packages
Paquetes exportables de objetos de configuración (BEs, páginas, eventos, scripts, plantillas, esquema) con dependencias en cascada. Añadir una Page a un paquete arrastra automáticamente todo lo que esa página referencia.
Flujo de trabajo habitual (exportar): haz clic en Create, da nombre y versión al paquete. Añade los objetos de nivel superior que te interesan, normalmente un puñado de páginas. Abre el panel de elementos enlazados para ver la lista en cascada (lo que el paquete contendrá realmente). Desmarca lo que quieras omitir. Decide si incluyes datos (conmutador al exportar). Haz clic en Export, obtienes un ZIP.
Flujo de trabajo habitual (importar): en el entorno destino, abre Packages, haz clic en Import, sube el ZIP. La plataforma muestra qué se añadirá o actualizará. Los deltas de esquema no se aplican automáticamente, si el paquete referencia una columna que no existe en el destino, la importación falla.
Trampas: los identificadores son por nombre, no por ID numérico. Una BE llamada "customer" en origen se vincula a una BE llamada "customer" en destino, renombra cualquier lado y el enlace se rompe.
En git: la extensión de VS Code baja un paquete a una carpeta como un archivo por artefacto, y vuelve a enviar la carpeta, así que un paquete se puede revisar y versionar como cualquier otro código.
Third-Party Data Connections
Conexiones a bases de datos externas (Postgres, SQL Server, MySQL, otras) que importan tablas según un horario. Las tablas importadas se comportan como tablas nativas, aparecen en Schema Designer y pueden ser referenciadas por Business Entities, Pages y Scripts.
Flujo de trabajo habitual: crea una conexión (cadena de conexión, prueba, guarda). Lista las tablas remotas disponibles; elige cuáles importar. Por cada tabla importada, establece el modo de sincronización (Full / Delta) y el disparador de sincronización (cron / manual / intervalo fijo).
Replicado, no federado: los datos importados viven en tu propia
omnicore-db. Las lecturas son rápidas (Postgres local). Contrapartida: el desfase
entre sincronizaciones.
Columnas gestionadas por la aplicación: marca algunas columnas como "managed by Archestack", no se sobrescribirán por sincronizaciones futuras. Útil cuando quieres anotar los datos del proveedor con tus propias banderas de estado.
Trampas: la primera sincronización de una tabla grande puede tardar un rato.
Las sincronizaciones delta requieren una columna que el conector pueda usar como marca de
agua, normalmente una marca temporal modified_at.
AI Assistant
Un asistente de chat integrado en la aplicación que puede tanto responder preguntas sobre tus datos como cambiar tu entorno. Pregunta "¿cuántas órdenes de trabajo abiertas por sucursal este mes?" y consulta y responde. Pide "añade una columna de estado a las citas de servicio y una página para listarlas" y, si eres administrador, lo construye.
Trae tu propio modelo. Archestack no incluye ningún LLM ni paga ninguna inferencia. Un administrador añade un proveedor en la pantalla AI Configuration: una base URL, un nombre de modelo y una clave de API. Funciona todo lo que hable la API chat-completions de OpenAI (ChatGPT, DeepSeek, OpenRouter, Groq, Together, un Ollama o vLLM autoalojado), y la propia API de Anthropic está soportada directamente. Puedes configurar varios y elegir uno por conversación.
Todo lo que hace pasa por el MCP Server. El asistente no tiene una puerta trasera propia a la plataforma, llama a las mismas herramientas que cualquier cliente MCP externo, con tu identidad. Los límites que fijas allí también se le aplican a él, y cada acción acaba en el mismo registro de auditoría. Consulta MCP Server más abajo.
Flujo de trabajo habitual: abre el asistente desde la barra lateral o la burbuja flotante. Pregunta en una o dos frases. Lee tu esquema actual antes de proponer nada, te dice qué va a cambiar y espera tu confirmación. Cada llamada a herramienta aparece bajo la respuesta, así que ves lo que hizo realmente.
Funciona bien para: preguntas puntuales sobre tus datos, funcionalidades con forma de CRUD, automatizaciones simples, extensiones a entidades existentes.
Menos bien para: reglas de negocio no obvias y flujos complejos de varios pasos. Trata lo que construye como la PR de un desarrollador junior.
Trampas: trabaja sobre datos en vivo y no hay deshacer, así que haz una copia de seguridad antes de algo arriesgado. Tus prompts y lo que devuelvan las herramientas se envían al proveedor que configuraste, bajo sus condiciones; apúntalo a un modelo autoalojado si eso importa. Las conversaciones largas solo repiten sus turnos más recientes, así que un chat abierto todo el día no recordará el principio. Y fíjate en los dos interruptores: con el MCP Server parado el asistente aún puede hablar, pero no puede leer ni cambiar nada.
MCP Server
Cada entorno puede exponerse mediante el Model Context Protocol, para que un cliente de IA fuera de Archestack, Claude Desktop, Claude Code, ChatGPT deep research, un IDE o tu propio script, pueda trabajar con él directamente. También es sobre lo que funciona el AI Assistant integrado.
Desactivado por defecto, y las herramientas que cambian cómo está construida la aplicación siguen desactivadas incluso después de arrancarlo. Es deliberado.
Tres grupos de herramientas. Read cubre listar y describir entidades, consultar filas y leer el esquema, las páginas, los eventos, los script modules, los packages, las translations y los event logs. Write es crear, actualizar y eliminar registros, y pasa por el mismo camino que la interfaz, así que tus Business Events se disparan exactamente igual que para una persona que pulsa Guardar. Admin es el grupo que permite a un modelo construir la aplicación en sí: entidades, esquema, páginas, eventos, script modules, translations, packages y los paquetes NuGet que usan los scripts.
Cuatro puertas deciden si una llamada llega: ¿está el servidor en marcha?, ¿está permitido el grupo?, ¿está habilitada esa herramienta concreta? y ¿lleva el token el scope? Una herramienta bloqueada por cualquiera de ellas es invisible para el cliente, en lugar de rechazada después.
Tokens. Emite uno por cliente en la pantalla MCP Server, con los scopes que realmente necesita y una caducidad opcional. El secreto se muestra una sola vez, al crearlo, y solo se almacena un hash, así que un token perdido se rota en lugar de recuperarse. Da a cada integración su propio token: mantiene legible el registro de actividad, y revocar uno no molesta a los demás.
Registro de actividad. Cada invocación queda registrada, incluidas las rechazadas y por qué. Aquí es donde compruebas qué ha estado haciendo realmente una integración.
Trampas: un token con el scope admin es tratado como administrador completo por todo lo que hay detrás, así que repártelos con cuidado. "Allow token in URL" existe para clientes que no pueden enviar una cabecera Authorization, pero una URL acaba en logs y capturas de pantalla mucho más fácilmente que una cabecera; déjalo desactivado salvo que lo necesites. Y en un entorno de producción con datos reales, deja el grupo admin desactivado.
Object Browser
Vista cruda de cada tabla nativa e importada, añade, edita, elimina filas directamente. Incluye una importación CSV para carga masiva.
Cuándo recurrir a él:
- Cargar datos de referencia en masa antes de que existan las páginas (países, monedas, enums de estado).
- Depuración, echar un vistazo a lo que hay realmente en una tabla cuando una página se comporta mal.
- Ediciones administrativas rápidas a datos que aún no están expuestos vía una Page.
- Importación CSV: elige una tabla, sube un archivo, mapea columnas a campos, vista previa, confirma.
Trampas: Object Browser sí dispara Business Events (pasa por la misma ruta de escritura EntityService que el front-end). La excepción es el SQL directo vía Database Deployments, que se salta todo.
Dependency Graph
Un mapa de solo lectura de cómo los objetos de configuración de tu entorno se referencian entre sí. Ocho tipos de recurso aparecen como nodos, coloreados por tipo: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events y Packages. Una arista entre dos nodos significa que un recurso depende del otro. Recurre a él para responder a las preguntas "¿qué se rompería si elimino esto?" y "¿hay algo sin usar aquí?".
Qué significa una arista: una Page vinculada a una Business Entity, un Event
Trigger registrado en una Business Entity, un paso de acción de Page que dispara un Event
Trigger, un script que llama a otro Script Module vía Modules.CallAsync("Name"),
un script que renderiza un PDF Template, un Scheduled Event que dispara un Event Trigger, o un
Package que contiene un recurso. Es una vista de design-time, refleja lo que tu
configuración cablea junto, no el tráfico de ejecución en vivo.
Dos tipos de problema se hacen destacar visualmente:
- Loose ends (huérfanos) - nodos a los que nada referencia y que no referencian nada. Se recogen en una banda "Loose ends" con borde rojo en la parte inferior del lienzo. Un Script Module que nadie llama; una Page vinculada a nada.
- Dangling references - dibujadas como aristas discontinuas rojas. La referencia existe en la configuración pero su destino no se puede encontrar, por ejemplo un paso de acción de Page que todavía apunta a un Event Trigger eliminado, o un script que llama a un Script Module por un nombre que ya no existe.
Flujo de trabajo habitual: abre Dependency Graph desde la sección Tools de la sidebar. El panel izquierdo tiene una búsqueda por nombre, un selector Layout (Left to right, Top to bottom, By type, Force-directed), los interruptores Group by package, Show loose ends y Show edges, y una lista de comprobación por tipo. Haz clic en un nodo para entrar en modo de enfoque: ese nodo y sus vecinos directos permanecen brillantes mientras el resto del grafo se atenúa, de modo que puedes rastrear exactamente qué toca un recurso. Haz clic en el fondo del lienzo para borrar el enfoque. Cada nodo lleva un pequeño botón de abrir en pestaña nueva que salta directamente al editor de ese recurso. La línea de estadísticas en la parte superior informa del número de nodos, aristas, huérfanos y dangling references.
Group by package: envuelve los miembros de cada package en una caja discontinua etiquetada, para que puedas ver un package como una unidad y detectar los recursos que arrastró por cascada.
Trampas: el grafo se construye bajo demanda a partir del estado actual, usa
el botón de refresco después de cambiar recursos en otro sitio. Las aristas derivadas de
scripts se encuentran escaneando los cuerpos de script en busca de la forma de llamada literal
Modules.CallAsync("Name"), un módulo invocado mediante un nombre calculado no
mostrará una arista. Una dangling reference siempre vale la pena corregirla; un huérfano a
menudo no, un Script Module recién creado cuenta como huérfano hasta que algo lo llama.
Event Logs
El rastro de auditoría de todo lo que la plataforma ha ejecutado, Business Events, Script Modules, Scheduled Events, sincronizaciones de terceros.
Flujo de trabajo habitual: abre Event Logs, establece Status a Failed, haz clic en una entrada. El panel de detalle muestra el payload (estado del registro en el momento, parámetros pasados), la excepción con la traza de pila y el tiempo.
Logs y Charts: la página tiene dos pestañas que comparten una misma fila de filtros. Logs es la tabla de arriba. Charts es un panel operativo sobre el mismo filtro: el volumen a lo largo del tiempo apilado por estado, una tendencia de la tasa de éxito, un desglose por source, los events que más fallan y los más lentos, la duración media y máxima a lo largo del tiempo, un desglose por tipo de trigger, y un mapa de calor de día de la semana por hora del día, con los totales principales arriba. Ambas pestañas comparten un rango de fechas Desde / Hasta (con los preajustes Últimas 1h / 24h / 7d); la pestaña Charts elige automáticamente un intervalo de tiempo adecuado para el rango y puede actualizarse a intervalos para monitoreo en vivo.
Notificaciones en tiempo real: el icono de campana en la barra superior de la aplicación de administración expone los fallos no leídos vía SignalR.
Trampas: el log puede crecer mucho. Las entradas antiguas se conservan para siempre por defecto; si el disco es una preocupación, configura un Scheduled Event que pode las entradas más antiguas que N días.
User Management
Crea, invita y gestiona usuarios; asigna roles del realm. Envuelve la API REST de administración de Keycloak.
Roles:
- admin - todo; puede gestionar todos los Business Units, usuarios, roles, ajustes del sistema.
- owner - gestiona sus propios Business Units, puede editar páginas de administración y de tiempo de ejecución.
- editor - puede ver y editar páginas de tiempo de ejecución, pero no cambiar la configuración de schema/BE/event.
- user - solo lectura en páginas de tiempo de ejecución.
Trampas: degradar a un usuario de admin a mitad de sesión no lo expulsa, el cambio se aplica en el siguiente refresco de token (típicamente en menos de un minuto).
Business Units
Grupos respaldados por Keycloak que restringen la visibilidad de recursos individuales. Consulta Conceptos -> Business Units para el modelo.
Flujo de trabajo habitual: crea un BU. Invita miembros por email y elige el rol. Asigna recursos yendo a cada BE / Page / Trigger o vía el panel de elementos enlazados del BU.
Asignaciones en cascada: cuando asignas una Page a un BU, la asignación se propaga a la BE a la que la página se vincula.
Trampas: el BU activo de un usuario se establece en el selector de BU
de la barra superior y persiste en localStorage. Los usuarios nuevos por defecto
están en su primer BU; si una página de lista está sospechosamente vacía, comprueba qué BU
está activo.
Database Backups
pg_dump nocturno de las tres bases de datos, más un ZIP con los
archivos del espacio de trabajo. Disparo manual / subida / restauración
desde la interfaz.
Flujo de trabajo habitual: Trigger now -> Download -> Upload -> Restore. El horario por defecto es nocturno a las 03:00 UTC.
Limitación de la prueba: los Backups están desactivados en el entorno de prueba.
Branding
Cambia el nombre mostrado de la aplicación, el logotipo (claro + oscuro), los colores, las fuentes y el resto del tema de Material UI, aplicado en tiempo de ejecución, sin recompilar.
Flujo de trabajo habitual: establece el nombre de la aplicación, sube un logo claro y un logo oscuro (PNG/JPG/SVG, ≤ 10 MB), elige los colores de marca en la pestaña Theme. Guarda: tu sesión lo muestra al instante, las de los demás en su próxima carga de página.
Tema: la pestaña Theme es un editor completo de temas de Material UI con vista
previa en vivo en modo claro y oscuro: los colores de marca y cada color de la paleta por modo,
las fuentes (cualquier Google Font) y cada estilo de texto, el radio de las esquinas, el
espaciado, las sombras, la velocidad de las animaciones, el estilo de botones, tarjetas y
campos, los valores por defecto de los componentes y sus overrides en JSON. Comprueba el
contraste, parte de plantillas, importa un tema de otra app de Material UI y exporta este como
código. Show on the whole app prueba un borrador en todas las pantallas antes de
guardar. Un workspace que nunca cambia su tema conserva exactamente el aspecto integrado. En un
repositorio el tema es la clave theme de workspace/branding.json.
Trampas: el logo es también el icono de la pestaña del navegador y el icono que muestra un teléfono para la aplicación en su pantalla de inicio. La pantalla de inicio necesita un logo PNG, JPG o WebP; con un SVG muestra el icono de Archestack.
Analytics
Google Analytics en la aplicación publicada, configurado en la página Analytics (Configuration). El builder nunca se rastrea.
Flujo habitual: pega el ID de medición de GA4 (G-XXXXXXXXXX, en Administrar, Flujos de datos de Google Analytics), marca Track visits to the published app y guarda. Ask visitors for consent first está activado por defecto: la etiqueta de Google solo se carga después de que un visitante acepte el banner, como exigen las normas de cookies de la UE. El texto del banner, sus botones y un enlace a tu política de privacidad pueden ser los tuyos.
Eventos desde el código de página: una vez cargada la etiqueta, el código de página puede enviar sus propios eventos, como window.gtag?.("event", "sign_up"). No se envía nada mientras el rastreo esté desactivado o el visitante lo haya rechazado.
Translations
Editor clave/valor para las cadenas de UI de la aplicación. Añade nuevos locales, sobrescribe las etiquetas por defecto por cliente.
Flujo de trabajo habitual: elige un locale, busca una clave, escribe un nuevo valor, guarda. Refresca la aplicación de administración.
Trampas: las traducciones se cargan una vez por sesión; los usuarios con una pestaña obsoleta no ven los cambios hasta que refresquen. Algunas etiquetas (las cabeceras de columna de las BEs) vienen de la configuración de la BE, no de las traducciones.
APIs invocables desde scripts
La superficie exacta que los scripts pueden invocar. Verificada contra el código fuente, cada método de abajo existe realmente.
Globales
Cada script obtiene estas variables de nivel superior (no existen otros nombres en el ámbito del script):
Entity-dynamicExpandoObject. Registro actual. Accede a los campos conEntity.column_name. MutarEntityen un trigger Before* persiste los cambios como parte de la escritura en curso.OldEntity-dynamicExpandoObject. Registro anterior (triggers de update + delete). De solo lectura.Log-ILogger.Log.LogInformation("…"),Log.LogWarning(…),Log.LogError(…). Escribe en los logs del servidor y en la entrada del Event Log.Db-DbHelper. Acceso a base de datos: un conjunto tipado por cada Business Entity, además de los helpers no tipados (ver más abajo).Modules- llama a otros Script Modules. Cada módulo es un método con sus propios parámetros:await Modules.OrdersOf(customer: "Acme"), comprobado al compilar el script; el resultado esdynamic.await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" })sigue llamando a un módulo por nombre y devuelveTask<object?>.Pdf-ScriptPdfHelper. Renderiza una PDF Template almacenada por nombre:await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() })devuelvebyte[];await Pdf.GenerateBase64Async(…)devuelve el mismo payload codificado en base64. Los parámetros se reenvían al data script de la plantilla como variables locales tipadas como string.Ocr-ScriptOcrHelper. Lee el texto impreso de una imagen, en el propio proceso, sin servicio externo ni API key:await Ocr.ExtractAsync(byte[] image)devuelve unOcrExtraction.await Ocr.ExtractBase64Async(string)toma la misma imagen codificada en base64 (se tolera un prefijodata:image/png;base64,),await Ocr.ExtractTextAsync(byte[])devuelve solo el texto.Doc-ScriptDocHelper. Encuentra el documento en una foto y lo aplana:await Doc.CropAsync(byte[] image)devuelve unDocumentCrop.await Doc.CropBase64Async(string)toma una captura en base64.Files-ScriptFilesHelper. La carpeta de archivos propia del workspace, la que gestiona la página Files:await Files.WriteAsync("invoices/1042.pdf", bytes),await Files.ReadAsync(path),ListAsync(folder),MoveAsync,DeleteAsync. Ver Files.
No hay User, Http, Email,
Templates ni ningún otro global.
Db - métodos de fila única + escritura
Todos async, todos sobre el objeto Db de nivel superior:
await Db.GetAsync(string table, object id)- obtiene una fila por la clave primaria. Devuelvedynamic?(onullsi no se encuentra).await Db.CreateAsync(string table, object values)- inserta.valuespuede ser un objeto anónimo o unDictionary<string, object?>. Dispara los triggers Before/After Create. Devuelveint(el nuevo ID).await Db.UpdateAsync(string table, object id, object values)- actualiza por PK. Dispara los triggers Before/After Update.await Db.DeleteAsync(string table, object id)- elimina por PK. Dispara el trigger Before Delete.
Db - constructor de consultas fluido
Empieza con Db.From(table). Encadena filtros, luego un terminal:
.Where(string column, string op, object? value = null)- añade una cláusula WHERE. Operadores:=,!=/<>,>,>=,<,<=,LIKE,ILIKE,IN,IS NULL,IS NOT NULL. Encadena varias llamadas a.Where(...)para AND..OrderBy(string column, bool desc = false)- establece ORDER BY. Solo se permite uno por consulta..Limit(int limit)- máximo de filas. El valor por defecto es 1000.
Terminales (cada uno ejecuta la consulta):
await ...ToListAsync()- devuelveList<dynamic>.await ...FirstAsync()- devuelvedynamic?(la primera coincidencia o null). Establece temporalmente el límite a 1.await ...CountAsync()- devuelveint.
No existe SumAsync, MaxAsync,
FirstOrDefaultAsync ni SingleAsync sobre Db.From. Para
agregaciones, recupera con ToListAsync() y reduce en C#, o usa los conjuntos
tipados de más abajo, que los tienen todos.
Ejemplo trabajado
// Get a row by ID, query a list, calculate a sum, update.
var order = await Db.GetAsync("orders", 42);
if (order == null) return false;
var items = await Db.From("order_items")
.Where("order_id", "=", order.id)
.OrderBy("id")
.ToListAsync();
decimal totalAmount = 0;
foreach (var item in items) {
totalAmount += (decimal)(item.amount ?? 0);
}
await Db.UpdateAsync("orders", 42, new {
total = totalAmount,
updated_at = DateTime.UtcNow
});
return true; Db - Business Entities como conjuntos tipados
Cada Business Entity es también un conjunto tipado sobre Db, que lleva el nombre de
la entidad sin espacios: una entidad llamada Service Order es Db.ServiceOrder.
Por debajo es EF Core, así que funciona todo LINQ (Where, Include,
SumAsync, GroupBy, proyecciones), con una navegación por cada clave
foránea y por cada join de Business Entity. Las columnas son propiedades en PascalCase con sus
tipos reales: planned_date es PlannedDate, un DateOnly.
// Every business entity is a typed set on Db, named after the entity.
var since = new DateOnly(2026, 1, 1);
var open = await Db.Order
.Where(o => o.Status == "Open" && o.OrderDate >= since)
.Include(o => o.OrderItems)
.OrderBy(o => o.OrderDate)
.ToListAsync();
foreach (var order in open)
order.Total = order.OrderItems.Sum(i => i.Amount ?? 0);
var customer = await Db.Customer.SingleAsync(c => c.Email == "ada@example.com");
Db.Order.Add(new Order { Customer = customer, Status = "Open" });
// Writes every changed, added and removed row; business events fire as usual.
await Db.SaveChangesAsync();
return open.Count; - Nombres. Una tabla en la que no se basa ninguna Business Entity recibe un conjunto con el nombre de la tabla (
bank_accountesDb.BankAccount). Una columna de clave foránea sin su sufijo_ides la navegación:customer_idesCustomer, y el otro lado es el plural,Customer.Orders. - Guardado.
await Db.SaveChangesAsync()escribe cada fila añadida, modificada o eliminada igual que lo hacenCreateAsync,UpdateAsyncyDeleteAsync, así que se disparan los Business Events y se sellan las columnas de auditoría, y una fila modificada envía solo las columnas que cambiaron. No es una única transacción: las filas guardadas antes de un fallo siguen guardadas.ExecuteUpdate,ExecuteDeletey las escrituras con SQL crudo se rechazan. - Tipos. Un
timestamp with time zonese lee como unDateTimeen UTC, y un valor que se compare con él también debe estar en UTC:DateTime.UtcNow. Las columnas de un tipo que el modelo no mapea (arrays, enums, tipos geométricos) se quedan fuera; léelas conDb.From. - Siempre al día. El modelo se reconstruye cada vez que cambia una tabla o una Business Entity, así que una columna eliminada es un error de compilación en su propia línea en lugar de un fallo en tiempo de ejecución. Un despliegue pendiente enumera, antes de que lo ejecutes, los scripts tipados que dejarían de compilar.
Db.From y los métodos de arriba siguen funcionando junto a los conjuntos tipados, y
tanto el editor de scripts como VS Code autocompletan los conjuntos, las columnas y las
navegaciones.
Modo de simulación
Durante la simulación de trigger (el botón Run Simulation en un Business Event) y en la
pestaña Test de un Script Module, Db.SimulationMode vale true. Las
escrituras (CreateAsync, UpdateAsync, DeleteAsync y
SaveChangesAsync) comprueban la tabla, la fila y los valores, registran lo que
habrían hecho y no escriben nada, así que tampoco se dispara ningún Business Event. Las lecturas
ven los datos reales. Las escrituras, movimientos y borrados con
Files también se simulan, y el correo y las notificaciones se
registran, no se envían. Esto es lo que hace seguro usar Run Simulation contra datos
reales. Todo lo demás que hace el script por su cuenta es real: las llamadas HTTP,
System.IO y todo lo que hace un paquete NuGet, como una
conexión a otra base de datos.
Modules
await Modules.OrdersOf(customer: "Acme")- cada Script Module es un método con el nombre del módulo sin espacios ("Orders of" esModules.OrdersOf) que recibe los parámetros que declara con sus tipos, primero los obligatorios; si omites uno opcional, recibe su valor por defecto. Un nombre mal escrito o un valor del tipo equivocado es un error de compilación en su línea. DevuelveTask<dynamic?>, así que lees el resultado sin cast, incluso un tipo anónimo. Los argumentos leídos deEntityson dynamic: haz un cast ((int)Entity.company_id) para que la llamada se compruebe al compilar.await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null)- llama a otro Script Module por nombre. Los parámetros se convierten en variables tipadas en el script del módulo llamado. DevuelveTask<object?>(lo que el módulo haya devuelto).
Files - la carpeta del workspace
Cada workspace tiene en el servidor una carpeta propia, que se gestiona en la página
Files (Operations). Allí llegan las subidas, y también todo lo que un script
guarda: una factura generada, una exportación, un modelo entrenado. Nada de ello se guarda en una
base de datos, y ninguna ruta llega a la carpeta de otro workspace. Las rutas son relativas y usan
barras normales; se rechaza una ruta con .., una unidad o un enlace.
// A generated invoice, kept in the workspace's own folder.
var pdf = await Pdf.GenerateAsync("Invoice", new Dictionary<string, string> { ["id"] = Entity.orders_id.ToString() });
var path = $"invoices/{DateTime.UtcNow:yyyy}/{Entity.orders_number}.pdf";
await Files.WriteAsync(path, pdf);
// Read it back, or anything an admin uploaded on the Files page.
var rates = await Files.ReadTextAsync("config/vat-rates.json");
foreach (var file in await Files.ListAsync("invoices", recursive: true))
Log.LogInformation("{Path} is {Size} bytes", file.Path, file.Size);
return path; ReadAsync(path)devuelve los bytes y lanza una excepción si el archivo no existe;ReadTextAsynclee texto UTF-8;ReadDataUriAsyncdevuelvedata:image/png;base64,…para una imagen en una PDF Template o en un correo.WriteAsync(path, bytes)yWriteTextAsynccrean las carpetas y reemplazan un archivo existente. Una escritura se completa entera o no se hace.ListAsync(folder, recursive)lista primero las carpetas; cada entrada tienePath,Name,IsFolder,Size,ModifiedUtcyContentType.MoveAsync(from, to)nunca sobrescribe;DeleteAsync(path)borra un archivo, o una carpeta con todo su contenido.PathOf(path)es la ruta absoluta en disco, para una biblioteca que necesite una.
En una ejecución de prueba (el botón Test) y en una ejecución de tests, las escrituras,
movimientos y borrados nunca llegan a la carpeta: la ejecución los guarda para sí, así que leer un
archivo recién escrito funciona, y los muestra junto a lo que registró. La carpeta
.archestack contiene los archivos propios de la plataforma (logos, iconos de
paquetes, adjuntos de correo): los scripts pueden leerla, nunca escribirla. Una copia de seguridad
incluye la carpeta como un archivo workspace-files junto a los volcados de las bases
de datos.
Ocr y Doc - leer una foto
Ambos corren dentro del proceso de la API. No hay servicio de OCR que desplegar, ni clave que configurar, ni coste por página.
OcrExtraction (lo que devuelve Ocr):
Text- cada línea reconocida unida con saltos de línea, en orden de lectura.Lines- por líneaText,Confidencey una bounding box:X,Y,Width,Height, másRightyBottom. Píxeles de la imagen origen, origen arriba a la izquierda.Confidence- media de las confidencias por línea, 0 a 1.ElapsedMs,IsEmpty.
Usa las cajas. Leer un documento significa localizar una etiqueta y luego leer lo que hay al
lado, que el texto por sí solo no puede expresar: busca la línea que contiene "Total" y toma la
línea cuyo rango Y se solapa con ella y cuya X supera su
Right.
DocumentCrop (lo que devuelve Doc):
Found- si realmente se detectó un contorno. Compruébalo primero.Image- bytes PNG de la página aplanada, o los bytes originales sin tocar cuandoFoundes false. Una foto que nunca se recortó se sigue leyendo; una recortada al cuadrilátero equivocado no, así que el detector prefiere declinar antes que adivinar.Corners- el contorno en la foto origen, arriba-izquierda, arriba-derecha, abajo-derecha, abajo-izquierda.Width,Height,ElapsedMs.
Los dos se componen en el orden evidente, y como un recorte fallido devuelve el original,
encadenarlos siempre hace algo: var crop = await Doc.CropAsync(photo); var scan = await
Ocr.ExtractAsync(crop.Image);
Tres límites que conviene diseñar de antemano. El modelo incluido es PP-OCRv5
latin, o sea alfabetos occidentales y dígitos, no CJK. No es un lector de PDF, una página PDF
tiene que rasterizarse a imagen antes. Y esto es OCR, no comprensión de documentos: convertir
líneas en proveedor, número de factura y total es lógica que escribes tú, y a veces se
equivocará, así que revisa line.Confidence y envía las lecturas bajas a una persona
en vez de escribirlas directamente en una tabla.
Cuidado con el timeout del script. Una página completa tarda uno o dos segundos y la primera
llamada del proceso añade cerca de un segundo cargando los modelos. El timeout de diez segundos
no interrumpe un await, así que un script que lee varias páginas no devuelve nada en
lugar de fallar pronto. Haz el trabajo multipágina en un Scheduled Event, una página por
ejecución.
Expresiones de plantilla (acciones de Business Event)
La configuración de acciones usa llaves {{ … }}. La lista completa de tokens válidos:
{{ Entity.column_name }}/{{ Entity.id }}{{ OldEntity.column_name }}{{ now() }}/{{ getdate() }}- datetime ISO 8601 UTC{{ today() }}- cadena de fecha{{ guid() }}/{{ newid() }}- GUID nuevo{{ year() }},{{ month() }},{{ day() }},{{ timestamp() }}- segundos Unix{{ empty() }},{{ null() }}- Agregados dentro de un contexto de join:
{{ SUM(column) }},{{ AVG() }},{{ COUNT() }},{{ MIN() }},{{ MAX() }} - Aritmética:
{{ Entity.quantity * Entity.price }}- evaluada tras la sustitución medianteDataTable.Compute()
No hay token {{ user.email }} ni ningún otro token con ámbito de usuario.