i18n - docs translations (#22617)

Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-07-07 11:54:54 +02:00
committed by GitHub
parent 07a921f8ca
commit 18ca89bcdd
97 changed files with 13247 additions and 0 deletions
@@ -0,0 +1,81 @@
---
title: 5. An AI agent
icon: robot
description: Deja que un agente genere documentos de un chat, usando tu herramienta.
---
Debido a que `generate-document` está expuesto como una **herramienta**, un agente de IA puede llamarlo.
Añadamos un agente y una habilidad para que los usuarios puedan decir *"generar una propuesta para
Grifo de Jeffery"*.
## La habilidad
Una [skill](/l/es/developers/extend/apps/logic/skills-and-agents) es reutilizable
instrucciones: conocimiento que adjuntas a los agentes. La nuestra enseña al modelo cómo usar
la herramienta.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## El agente
Un [agent](/l/es/developers/extend/apps/logic/skills-and-agents) empareja un símbolo de espera de órdenes con un modelo
. Establece `responseFormat` explícitamente para evitar una advertencia de compilación.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
El agente sólo puede llamar a la herramienta si su papel lo permite. Ya establecemos
`canAccessAllTools: true` y `canBeAssignedToAgents: true` en el rol de la aplicación en
[Capítulo 2](/l/es/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Pruébalo
Abre un chat con **Asistente de documentos** y pídele que elabore un documento para una personaformat@@0
en tu CRM. Encuentra el registro, llama a `generate-document` e informa
el documento que creó, el cual ahora aparece en tu vista de **Documents**,
exactamente igual que las rutas del menú de comandos y del flujo de trabajo.
Esa es la ganancia de exponer la lógica como una herramienta: **una función, muchas puertas delanteras** —
menú de comandos, HTTP, paso de flujo de trabajo y ahora lenguaje natural.
**Después de este paso:** la aplicación es completa y genuinamente útil. Es hora de
enviarlo.
<Card title="Siguiente: publicando →" icon="cohete" href="/Developopers/extend/apps/tutorials/document-generator/publishing">
Añadir metadatos de mercado y publicar.
</Card>
@@ -0,0 +1,304 @@
---
title: 4. Construyendo la interfaz de usuario
icon: table-columns
description: Visualizaciones, navegación en la barra lateral, un comando y componentes frontales.
---
Ahora mismo los objetos sólo son accesibles a través de Configuración. Vamos a dar a la aplicación una presencia
real en la interfaz de usuario: vistas de lista, entradas de la barra lateral, un comando de
**Generar documento**, un componente frontal de página de registro para **previsualizar** un documento
y una pestaña de **editor** de texto nativo para plantillas.
## Vistas y navegación
Una [view](/l/es/developers/extend/apps/layout/views) es una lista guardada de un objeto dado.
Un [elemento del menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items)
pone esa vista en la barra lateral.
```ts filename="src/views/documents.view.ts"
import { defineView, ViewKey } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
key: ViewKey.INDEX,
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Añadir el mismo par para las plantillas. Ambos ahora se muestran en la barra lateral:
<Frame caption="Documentos y plantillas en la barra lateral con el documento generado listado.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/04-documents-view.png" alt="Vista de documentos con un documento generado" />
</Frame>
## Un componente frontal
Un [componente delantero](/l/es/developers/extend/apps/layout/front-components) es un componente de React
en el interior de Twenty. Nuestra nuestra lee el registro seleccionado, carga las plantillas
persona a través de `CoreApiClient`, y POSTs a la ruta desde el último capítulo
.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const apiBaseUrl = process.env.TWENTY_API_URL;
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ templateId, recordId }),
}).then((r) => r.json());
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Estilo con variables CSS en línea (`var(--t-color-blue)`), no valores importados de
`twenty-ui`. El SDK simula ese paquete durante la compilación, por lo que las importaciones a nivel de módulo de
constantes de tema serían `undefined`. Ver el
[componente completo](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Un comando para abrirlo
Un [elemento del menú de comandos](/l/es/developers/extend/apps/layout/command-menu-items) con
`availabilityType: 'RECORD_SELECTION'` aparece cuando se selecciona una Persona, y
abre el componente en el panel lateral.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Prueba todo el flujo
Abre **Personas**, marca a una persona y pulsa <kbd>mañK</kbd> / <kbd>Ctrl K</kbd>.
"Generar documento" aparece, etiquetado con tu aplicación:
<Frame caption="El comando se muestra cuando se selecciona una Persona.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/06-command-menu.png" alt="Menú de comandos con Generar documento" />
</Frame>
Ejecutarlo — su componente se abre en el panel lateral. Elige una plantilla, haz clic en
**Generar**, y un nuevo registro de tierras en **Documentos**.
<Frame caption="El componente frontal, cargando plantillas y generando al hacer clic.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/06b-front-component.png" alt="Generar panel lateral del documento" />
</Frame>
Cada documento generado registra tu aplicación como su autor:
<Frame caption="Creado por Document Generator, estado Generado.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/05-document-record.png" alt="Un registro de documento generado" />
</Frame>
## Vista previa de un documento en su página de registro
Un componente frontal no solo para los menús de comandos: puedes montar uno como una \*\*pestaña en una página de registro
. Añadamos una pestaña de *Vista previa* al registro de documentos que renderiza el cuerpo de Markdown
como una página pulida e imprimible.
El componente lee el id de registro actual de su contexto de ejecución, carga el documento
y lo renderiza. Los componentes de Front se ejecutan en una **sandbox** que solo permite una
lista blanca de etiquetas HTML: la inyección de HTML sin procesar (`dangerouslySetInnerHTML`) y
`\<style>` están bloqueados, por lo que representamos el Markdown como elementos de React con estilos
inline mediante un pequeño helper [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx).
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Montarlo con un [diseño de página](/l/es/developers/extend/apps/layout/page-layouts). Un diseño
`RECORD_PAGE` añade pestañas a la vista de registro de un objeto; un widget `FRONT_COMPONENT`
en una pestaña `CANVAS` aloja el componente:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Abre cualquier documento — una pestaña de **Vista previa** lo renderiza hermosamente, con enlaces a la página web compartible
y el PDF:
<Frame caption="La pestaña Vista previa muestra el documento con estilos en línea, además de enlaces rápidos.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/09-document-viewer.png" alt="Componente frontal del visor de documentos en una pestaña de página de registro" />
</Frame>
## Editar una plantilla con el editor de texto
Las plantillas no necesitan ningún componente personalizado. Porque el `body` es un campo
`RICH_TEXT`, Veinte ya proporciona un editor de texto completo para él — el mismo
que los objetos estándar de Nota y Task usan. Acabamos de superarlo en la página de registro
plantilla.
Añade una pestaña con un widget `FIELD` en el modo de visualización `EDITOR`, apuntando al campo `body`
a través de `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Un campo `RICH_TEXT` almacena tanto el bloque JSON del editor como una proyección
de Markdown. La generación de pipeline lee que Markdown proyecta, así que
marcadores de posición, el PDF, y la página web compartible siguen funcionando sin cambios —
vea el
completo [`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Ahora editores escriben plantillas en un editor de texto rich:
<Frame caption="La pestaña Plantilla: Editor nativo de texto de texto de 20 años vinculado al campo del cuerpo.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/10-template-editor.png" alt="Registro de plantillas con la pestaña nativa del editor de texto" />
</Frame>
**Después de este paso:** vista previa de documentos hermosamente y plantillas son editables
en la aplicación. A continuación, deja que un agente de IA los genere a partir de un chat.
<Card title="Siguiente: un agente de IA →" icon="robot" href="/Developopers/extend/apps/tutorials/document-generator/ai-agent">
Añade un agente y una habilidad que llame a tu herramienta.
</Card>
@@ -0,0 +1,135 @@
---
title: 1. Modelo de datos
icon: database
description: Modelo de documentos y plantillas con objetos, campos y una relación.
---
Nuestra aplicación necesita dos objetos personalizados: **plantillas de documentos** (qué escribir) y
**documentos** (el resultado generado). Vamos a definirlos.
Escaffold cada archivo de entidad con la CLI — genera un UUID válido y la carpeta
correcta para ti:
```bash filename="Terminal"
yarn twenty dev:add object
```
A continuación mostramos los archivos terminados.
<Note>
Cada `*_UNIVERSAL_IDENTIFIER` vida constante en
`src/constants/universal-identifiers.ts` y es importado donde se utiliza. Los fragmentos
a continuación omiten las importaciones por brevedad — manténgalos en tus propios archivos.
</Note>
## El objeto de plantilla
Una plantilla tiene un `nombre`, un `cuerpo` con `{{placeholders}}`, y un `target` que
dice si está escrito para una persona o una empresa. El `body` es un campo
`RICH_TEXT`, así que Veinte le da un editor completo de texto.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
`SELECT` option **values** debe ser `UPPER_CASE` (`PERSON`, no `person`), y el
`defaultValue` está envuelto en comillas adicionales: `` `'PERSON'` ``. La `label` es lo que
los usuarios ven.
</Warning>
## El objeto del documento
El documento generado almacena el `contenido` renderizado y un `status`. Definirlo
de la misma manera, con un `status` seleccionado de `DRAFT` / `GENERATED`. Archivo completo:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Vinculándolas con una relación
Cada documento debe apuntar de nuevo a la plantilla de la que proviene. Las relaciones son
**bidireccionales** — defines ambos lados, cada uno en su propio archivo de campo.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
El otro lado (`template-documents-relation.field.ts`) es un campo
`RelationType.ONE_TO_MANY` llamado `documents` que apunta a la manera opuesta.
Ver [Relations](/l/es/developers/extend/apps/data/relations) para ver el patrón completo.
## Ver en Veinte
Con `yarn veinte dev` en ejecución, abre **Ajustes → Modelo de datos**. Ambos objetos
aparecen, etiquetados con tu aplicación.
<Frame caption="Ambos objetos personalizados, propiedad del generador de documentos.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/01-data-model.png" alt="Configuración del modelo de datos mostrando plantillas de documentos y documentos" />
</Frame>
Crea una plantilla para probar con — nombra la *propuesta de ventas*, establece el **objetivo** a
*Persona*, y pega un cuerpo con unos pocos marcadores de posición:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Un registro de plantilla. El cuerpo mantiene sus marcadores de posición hasta que se genere un documento.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/03-template-record.png" alt="Un registro de plantilla de propuesta de ventas con el cuerpo del marcador de posición" />
</Frame>
**Después de este paso:** tienes los objetos `documentTemplate` y `document`, enlazados por
una relación, y una plantilla para generar. Después, la lógica que lo llena.
<Card title="Siguiente: generando documentos →" icon="bolt" href="/developopers/extend/apps/tutorials/document-generator/generating-documents">
Escriba la función lógica que llena la plantilla.
</Card>
@@ -0,0 +1,239 @@
---
title: 2. Generando documentos
icon: bolt
description: Una función lógica, expuesta como una herramienta de IA y una acción de flujo de trabajo.
---
Ahora el núcleo: una [función lógica](/l/es/developers/extend/apps/logic/logic-functions)
que carga una plantilla y un registro, rellena los marcadores de posición y guarda un nuevo documento
.
Escribiremos la lógica de negocio una sola vez como un **handler**, y luego la expondremos mediante
diversos desencadenadores. Este capítulo conecta dos de ellos: una **herramienta de IA** y una
**acción de flujo de trabajo**.
## El ayudante de renderizado
Mantenga la lógica pura en su propio archivo para que sea fácil de unir-test. Esto arrastra un registro
en `{{dot.path}}` tokens y los sustituye.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
Debido a que este archivo no tiene efectos secundarios, puedes cubrirlo con pruebas unitarias rápidas
(`yarn test:unit`). Ver [Testing](/l/es/developers/extend/apps/operations/testing).
</Tip>
## El manejador
El manejador utiliza el [`CoreApiClient`](/l/es/developers/extend/apps/logic/logic-functions)
generado para leer y escribir datos CRM. Carga la plantilla, carga el registro de destino, rellena
el cuerpo y crea un `document`.
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues` ejecuta una consulta diferente para una Persona vs. una Empresa y aplana
el resultado — vea
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## Exponerlo como una herramienta y una acción de flujo de trabajo
Un solo `defineLogicFunction` puede llevar varios disparadores. Aquí, `toolTriggerSettings`
hace que sea llamable por agentes IA, y `workflowActionTriggerSettings` lo convierte en un paso
en el constructor de flujo de trabajo visual. Ambos describen su entrada con un esquema JSON.
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
El esquema de entrada es un esquema JSON simple que describe `templateId` y `recordId` —
see [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## Conceder acceso
Funciones lógicas ejecutadas como el rol de la aplicación. Necesita leer plantillas y registrar
y crear documentos, así que permita eso en `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE` permite a la función cargar el PDF generado en la siguiente sección.
Ver [Roles](/l/es/developers/extend/apps/config/roles) para obtener permisos más finos.
## Adjuntar un archivo PDF real
Un campo de texto renderizado es útil, pero los usuarios quieren un documento real. Vamos a generar un
**PDF** y almacenarlo en el registro como un archivo descargable.
Primero, da al objeto `document` un campo `ARCHIVOS` para mantener el PDF. Las aplicaciones suben
a sus campos de archivos **propios**, así que este campo es qué rutas cargar:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
Ahora renderice ese PDF. Una aplicación es un proyecto de nodo real, así que puedes añadir cualquier paquete npm
que necesites e importarlo como en cualquier otro lugar. Utilizamos **[pdf-lib](https://pdf-lib.js.org/)**
para dibujar el PDF y **[marked](https://marked.js.org/)** para analizar el cuerpo de Markdown
— el CLI los instala en el tiempo de ejecución de la función para ti:
```bash filename="Terminal"
yarn add pdf-lib marked
```
El ayudante completo es
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Analiza el Markdown en tokens con `marked.lexer`, y luego los dispone con
pdf-lib: encabezados reales, texto en **negrita**/*cursiva*, listas con viñetas y numeradas,
citas en bloque y líneas de separación: una representación A4 pulida y de varias páginas de la propia plantilla,
en lugar de un bloque de texto.
<Frame caption="El PDF generado: tipografía real y formato Markdown, renderizando el cuerpo de la plantilla.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07b-generated-pdf.png" alt="Un PDF pulido y generado por marcadores" />
</Frame>
<Note>
Las fuentes incorporadas de pdf-lib's usan la codificación WinAnsi, por lo que los acentos de WesternEuropean renderizan
fuera de la caja el ayudante mapea comillas inteligentes y guiones y deja caer caracteres que
no puede codificar. Renderizar escrituras no latinas (chino, árabe, cirílico) implicaría
incrustar una fuente Unicode.
</Note>
A continuación, suba y almacene la referencia en el registro. `uploadFile` rutas bytes
a tu campo de archivos propiedad de la aplicación; el `id` devuelto es lo que guardas:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
El documento generado ahora contiene un PDF descargable:
<Frame caption="El PDF generado, almacenado en el campo Archivo del documento.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/08-document-with-pdf.png" alt="Un registro de documento con un archivo PDF generado" />
</Frame>
<Note>
`uploadFile` solo se dirige a campos de archivos **propiedad de la app** (por lo que las cargas siempre requieren una
app que sea propietaria del campo, además del indicador de rol `UPLOAD_FILE`). Por eso el PDF
aterriza en el propio campo `file` del registro — el mismo patrón que el
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
usa para grabaciones.
</Note>
**Después de este paso:** cada documento generado tiene un PDF real y descargable. Pero
nada puede *llamar* al generador de la interfaz de usuario aún — para eso necesitamos una ruta HTTP.
<Card title="Siguiente: rutas HTTP →" icon="globo" href="/Developopers/extend/apps/tutorials/document-generator/http-routes">
Servir la función sobre HTTP y representar documentos como páginas web.
</Card>
@@ -0,0 +1,148 @@
---
title: 3. Rutas HTTP
icon: globe
description: Activa la función sobre HTTP y renderiza documentos como páginas web.
---
El mismo manejador también puede responder a peticiones HTTP. Añadiremos dos rutas:
* un endpoint **POST** para generar un documento, y
* un endpoint público **GET** que renderiza un documento como una página web imprimible.
Ambos usan `httpRouteTriggerSettings`. Las rutas de la aplicación se sirven bajo `/s` en tu servidor
Veenty (por ejemplo, `http://localhost:2020/s/documents/generate`).
## Ruta POST — generar bajo demanda
Esto reutiliza `generateDocumentHandler`, así que no hay lógica para repetir: solo un adaptador
fino que lee el cuerpo de la solicitud.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
El manejador compartido devuelve un `status` sugerido en caso de fallo, así que la ruta puede
responder con un código apropiado `4xx`/`5xx`. `isAuthRequired: true` significa que la persona que llama
debe presentar un token válido — el componente frontal en el siguiente capítulo pasa el token de acceso del usuario
automáticamente.
## Ruta GET — renderizar como una página web
Para devolver HTML en lugar de JSON, envuelve el cuerpo en un encabezado `Response` con una cabecera
`Content-Type`. Esta ruta es pública (`isAuthRequired: false`) por lo que un documento generado
puede ser compartido como un enlace.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` renderiza el cuerpo de Markdown a HTML (con [marked](https://marked.js.org/),
saneado) y lo deja caer en una limpieza, página imprimible que muestra sólo el contenido
plantilla — el mismo aspecto que el PDF y la vista previa dentro de la aplicación.
[Ver el ayudante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Pruébalo
Con una plantilla y una persona en tu espacio de trabajo, llama a la ruta (toma un token desde
**Ajustes → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Abra el documento devuelto en su navegador:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="La ruta pública GET renderiza el documento como una página imprimible.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07-rendered-document.png" alt="Una página web de documentos procesados" />
</Frame>
<Tip>
También puedes transmitir los registros de una función mientras pruebas con
`yarn twenty dev:function:logs`, o invocarlo directamente con
`yarn twenty dev:function:exec`.
</Tip>
**Después de este paso:** la aplicación puede generar documentos a través de HTTP y servirlos como
páginas web. Ahora hagámoslo utilizable sin `curl`.
<Card title="Siguiente: construyendo la interfaz de usuario →" icon="table-columns" href="/Developopers/extend/apps/tutorials/document-generator/building-the-ui">
Vistas, navegación, un comando y un componente frontal.
</Card>
@@ -0,0 +1,67 @@
---
title: "Tutorial: Generador de documentos"
icon: wand-magic-sparkles
description: Crea una aplicación real de Twenty que genere documentos personalizados a partir de los datos de tu CRM.
---
En este tutorial crearás **Document Generator**, una aplicación que convierte plantillas reutilizables en documentos personalizados usando los datos que ya tienes en tu CRM.
Escribe una plantilla una vez con `{{placeholders}}`, luego genera un documento completado para cualquier Persona o Empresa con un solo clic, desde el menú de comandos, desde un agente de IA o desde un flujo de trabajo.
<Frame caption="Una plantilla, generada para una persona específica, abierta como una página imprimible.">
<img src="/images/docs/desarrolladores/extends/apps/document-generator/07-rendered-document.png" alt="Un documento generado de propuesta de ventas" />
</Frame>
## Lo que aprenderás
Cada capítulo añade una capacidad. Al final habrás tocado la mayoría del SDK.
| Capítulo | Capacidad | Referencia |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [1. Modelo de datos](/l/es/developers/extend/apps/tutorials/document-generator/data-model) | Objetos, campos y una relación | [Data](/l/es/developers/extend/apps/data/overview) |
| [2. Generando documentos](/l/es/developers/extend/apps/tutorials/document-generator/generating-documents) | Una función lógica (herramienta (AI + acción de flujo de trabajo) que rellena una plantilla de Markdown y adjunta un PDF pulido | [Funciones lógicas](/l/es/developers/extend/apps/logic/logic-functions) |
| [3. Rutas HTTP](/l/es/developers/extend/apps/tutorials/document-generator/http-routes) | Ejecutando JSON y una página HTML compartible desde rutas | [Funciones lógicas](/l/es/developers/extend/apps/logic/logic-functions) |
| [4. Construir la interfaz de usuario](/l/es/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vistas, navegación, menú de comandos y componentes frontales que previsualizan un documento y editan una plantilla | [Diseño](/l/es/developers/extend/apps/layout/overview) |
| [5. An AI agent](/l/es/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + habilidad | [Habilidades y agentes](/l/es/developers/extend/apps/logic/skills-and-agents) |
| [6. Publicación](/l/es/developers/extend/apps/tutorials/document-generator/publishing) | Envíalo al mercado | [Publicación](/l/es/developers/extend/apps/operations/publishing) |
## Prerrequisitos
Deberías haber terminado el [Inicio rápido](/l/es/developers/extend/apps/getting-started/quick-start):
un servidor Veinte local corriendo en el puerto `2020` y el CLI autenticado en él.
Si no, andamio e inicia uno ahora:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
¿Prefieres leer el código terminado? La aplicación completa vive en
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Cada fragmento de abajo es copiado de él.
</Note>
## Cómo encaja la aplicación
<Frame>
<img src="/images/docs/desarrolladores/extends/apps/document-generator/how-it-fits.svg" alt="Una plantilla con marcadores de posición se genera en un documento pulido con un PDF, activado desde el menú de comandos, un agente IA, un flujo de trabajo o un enlace compartible" />
</Frame>
Escribes una **plantilla** una vez en un editor de texto rich, con `{{placeholders}}`. Al elegir una plantilla
y un registro CRM rellenan los marcadores de posición y almacenan un **documento** pulido
(con un archivo PDF). Todo lo demás — el menú de comandos, el agente IA,
el paso del flujo de trabajo, el enlace compartible — es sólo una manera diferente de activar que
un generador.
## Mantener este ciclo en ejecución
Deja `yarn twenty dev` corriendo en un terminal para el tutorial completo. Cada vez que
agregas o editas un archivo en `src/`, se vuelve a sincronizar con tu servidor en unos
segundos, para que puedas ver cómo cada capacidad aparece en la interfaz de usuario a medida que la construyes.
<Card title="Comenzar construcción →" icon="database" href="/Developopers/extend/apps/tutorials/document-generator/data-model">
Capítulo 1: documentos de modelo y plantillas.
</Card>
@@ -0,0 +1,137 @@
---
title: 6. Publicación
icon: rocket
description: Añade metadatos de mercado y publica tu aplicación.
---
Tu app funciona. El último paso es describirlo para el mercado y publicarlo.
## Añadir metadatos de mercado
La [configuración de la aplicación](/l/es/developers/extend/apps/config/application) lleva los enlaces
que se muestran en el mercado: autor, categoría, logotipo y soporte
. Pon un logotipo en `public/` y referencialo con `logoUrl`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logoUrl: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/es/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
El rol por defecto se declara con `defineApplicationRole()` en su propio archivo — usted
ya no pase `defaultRoleUniversalIdentifier` aquí.
</Tip>
También añade la palabra clave `twenty-app` a `package.json` para que la aplicación sea detectable:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Añadir capturas de pantalla de galería
Un listado de mercado se vende a sí mismo con capturas de pantalla. Soltar unos pocos PNGs en
`public/gallery/` y referenciarlos con `capturas de pantalla` — se renderizan como una galería
en la página de listado.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
screenshots: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Lance con la ganancia: haga la primera captura de pantalla el resultado terminado (un documento
generado), luego muestre cómo se activa y se autoriza. Usa capturas
nítidas y de alta resolución, son lo primero que un usuario ve.
</Tip>
Dale a `README.md` el mismo tratamiento: es la página principal de npm y GitHub.
Abre con la proposición de valor y una captura de pantalla, muestra las características de la línea de cabecera,
y mantén los detalles de construcción debajo del plano.
## Comprobar antes de enviar
Ejecuta las mismas puertas que CI hace:
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty dev --once --dry-run # preview the metadata diff
```
La ejecución seca imprime exactamente lo que cambiaría en el servidor sin aplicarlo —
una buena comprobación final de sanidad. Ver
[Testing](/l/es/developers/extend/apps/operations/testing) y
[Sincronizando y recuperando](/l/es/developers/extend/apps/operations/sync-and-recovery).
## Publicar
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` construye y publica a npm por defecto; `--private` sube un tarball
a un registro privado del servidor Veinte en su lugar. Para superficiar una aplicación publicada
en el mercado de una instancia, dispara una sincronización de catálogo:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Detalles completos y la lista de verificación de lanzamiento:
[Publishing](/l/es/developers/extend/apps/operations/publishing).
## Construiste una aplicación 🎉
En seis capítulos utilizaste la mayor parte de la superficie SDK:
* **Objetos, campos y una relación** para modelar los datos
* Una **función lógica** expuesta como una **herramienta de IA**, una **acción de flujo de trabajo**, y **rutas HTTP**
* **Vistas, navegación, un comando y un componente frontal** para la interfaz de usuario
* Un **agente + habilidad** para generación natural
* **Metadatos del mercado** y el flujo de publicación
La aplicación terminada está en
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## A dónde ir siguiente
<CardGroup cols={2}>
<Card title="Referencia de datos" icon="database" href="/l/es/developers/extend/apps/data/overview">
Cada tipo de campo, relación y opción de índice.
</Card>
<Card title="Referencia lógica" icon="bolt" href="/l/es/developers/extend/apps/logic/overview">
Activadores de eventos Cron y base de datos, el almacén clave-valor, conexiones OAuth.
</Card>
<Card title="Referencia del diseño" icon="table-columns" href="/l/es/developers/extend/apps/layout/overview">
Diseños de páginas, widgets de tablero y más superficies de interfaz.
</Card>
<Card title="Operaciones" icon="rocket" href="/l/es/developers/extend/apps/operations/overview">
CLI, prueba, control remoto, y CI.
</Card>
</CardGroup>