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: Deixe um agente gerar documentos a partir de um chat, usando sua ferramenta.
---
Porque `generate-document` é exposto como uma **ferramenta**, um agente de IA pode chamá-lo.
Vamos adicionar um agente e uma habilidade para que os usuários possam dizer apenas *"generate a proposal for
Jeffery Griffin"*.
## A habilidade
A [skill](/l/pt/developers/extend/apps/logic/skills-and-agents) é reutilizável
instruções — conhecimento que anexa a agentes. Nós ensinamos o modelo como usar
a ferramenta.
```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'),
});
```
## O agente
Um [agent](/l/pt/developers/extend/apps/logic/skills-and-agents) emparelha um prompt com um modelo
. Defina `responseFormat` explicitamente para evitar um aviso de construção.
```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>
O agente só pode chamar a ferramenta se o seu papel lhe permitir. Nós já definimos
`canAccessAllTools: true` and `canBeAssignedToAgents: true` on the app's role in
[Capítulo 2](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Experimente isso
Abra um chat com o **Assistente de Documentos** e peça para ele redigir um documento para uma
pessoa no seu CRM. Ele encontra o registro, chama `gerar documento` e relata
o documento que ele criou — que agora aparece na visão **Documentos**,
exatamente como o menu de comando e caminhos do fluxo de trabalho.
Esse é o benefício de expor a lógica como uma ferramenta: **uma função, muitas portas da frente** —
menu de comando, HTTP, passo do fluxo de trabalho e agora a linguagem natural.
**Após este passo:** o aplicativo está cheio de recursos e realmente útil. Hora de
enviar.
<Card title="Próximo: publicação →" icon="rocket" href="/desenvolvedores/adicionar/apps/tutorials/document-gerador/publicação">
Adicionar metadados do mercado e publicar.
</Card>
@@ -0,0 +1,305 @@
---
title: 4. Construindo a interface
icon: table-columns
description: Views, navegação da barra lateral, um comando e componentes iniciais.
---
No momento, os objetos só podem ser acessados através de Configurações. Vamos dar ao aplicativo uma presença
real na UI: listar exibições, entradas da barra lateral, um clique
**Gerar documento** comando, um componente frontal de record-page para **visualizar** um documento
e uma guia de **editor** nativa para templates.
## Visualizações e navegação
A [view](/l/pt/developers/extend/apps/layout/views) é uma lista salva de um determinado objeto.
Um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items)
coloca essa visualização na 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,
});
```
Adicionar o mesmo par para modelos. Ambos agora aparecem na barra lateral:
<Frame caption="Documentos e Modelos na barra lateral, com o documento gerado listado.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Visualização de documentos com um documento gerado" />
</Frame>
## Um componente frontal
A [front component](/l/pt/developers/extend/apps/layout/front-components) é um componente React
sandboxed dentro de Twenty. Ours lê o registro selecionado, carrega o modelo de pessoa
via `CoreApiClient`, e POSTs até a rota do ú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 com variáveis CSS inline (`var(--t-color-blue)`), não valores importados de
`25ui`. O SDK simula esse pacote durante a build, portanto, imports em nível de módulo de
constantes de tema seriam `undefined`. Veja o
[componente inteiro](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Um comando para abri-lo
Um [item de menu de comando](/l/pt/developers/extend/apps/layout/command-menu-items) com
`availabilityType: 'RECORD_SELECTION'` aparece quando uma pessoa é selecionada, e
abre o componente no painel 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,
});
```
## Experimente todo o fluxo
Abra **Pessoas**, marque uma pessoa e pressione <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>.
"Gerar documento" aparece, marcado com seu aplicativo:
<Frame caption="O comando aparece quando uma pessoa é selecionada.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu de comando com o documento gerado" />
</Frame>
Executá-lo — seu componente abre no painel lateral. Escolha um modelo, clique
**Gerar** e uma nova terra em **Documentos**.
<Frame caption="O componente inicial, carregando templates e gerando no clique.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Gerar painel lateral do documento" />
</Frame>
Cada documento gerado grava o seu aplicativo como seu autor:
<Frame caption="Criado pelo Gerador do Documento: Status Gerado.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Um registro de documento gerado" />
</Frame>
## Pré-visualizar um documento em sua página de registro
Um componente frontal não é apenas para menus de comando. Você pode montar um como uma \*\*aba de uma página de registro
. Vamos adicionar uma aba *Pré-visualização* ao registro de documento que renderiza o corpo
Markdown como uma página polida e impressa.
O componente lê o id de registro atual de seu contexto de execução, carrega o documento
e o renderiza. Componentes frontais executados em uma **sandbox** que só permite uma lista
branca de tags HTML — injeção HTML bruta (`dangerouslySetInnerHTML`) e
`\<style>` estão bloqueados — então renderizamos o Markdown como elementos React com estilos inline
através de um pequeno [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
helper.
```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,
});
```
Monte-o com um [layout da página](/l/pt/developers/extend/apps/layout/page-layouts). Um layout
`RECORD_PAGE` adiciona abas à vista de registro de um objeto; um widget `FRONT_COMPONENT`
em uma aba `CANVAS` hospeda o 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,
},
}],
}],
});
```
Abra qualquer documento — uma guia **Pré-visualizar** renderiza belíssima, com links para a página web compartilhável de
e o PDF:
<Frame caption="A aba de Pré-visualização renderiza o documento com estilos embutidos, mais links rápidos.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Visualizador de documentos do componente frontal em uma aba de registros" />
</Frame>
## Edite um modelo com o editor de texto rico
Os templates não precisam de um componente personalizado. Como o `body` é um campo
`RICH_TEXT`, Twenty já fornece um editor de rich text completo para ele — o
mesmo que os objetos padrão Note e Task usam. Nós apenas a apresentamos na
página de registro de template.
Adicione uma aba com um widget `FIELD` no modo de exibição `EDITOR`, apontando para o campo `body`
atravé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',
},
}],
}
```
Um campo `RICH_TEXT` armazena tanto o JSON de blocos do editor quanto uma
projeção em Markdown. O pipeline de geração lê essa projeção Markdown, então
espaços reservados, o PDF, e a página da web compartilhável continuam funcionando inalterado —
veja o
[`template-record. leiaute-idade.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Agora editores escrevem modelos em um editor de texto rico:
<Frame caption="A aba Modelo: editor de texto rico nativo de 20 vinculado ao campo de corpo.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Registro de modelo com a aba de editor de texto rico nativo" />
</Frame>
**Após este passo:** os documentos pré-visualizar lindo e os templates são editáveis
dentro do aplicativo. Em seguida, deixe um atendente da IA gerá-los a partir de um chat.
<Card title="Próximo: um atendente de IA →" icon="robot" href="/desenvolvedores/extend/apps/tutorials/document-generator/ai-agent">
Adicione um agente e uma habilidade que chama sua ferramenta.
</Card>
@@ -0,0 +1,135 @@
---
title: 1. Modelo de dados
icon: database
description: Modela documentos e templates com objetos, campos e uma relação.
---
Nosso aplicativo precisa de dois objetos personalizados: **modelos de documentos** (o que escrever) e
**documentos** (o resultado gerado). Vamos defini-los.
Crie cada arquivo de entidade com o CLI — gera uma pasta UUID válida e
correta para você:
```bash filename="Terminal"
yarn twenty dev:add object
```
Abaixo nós mostramos os arquivos concluídos.
<Note>
Todo `*_UNIVERSAL_IDENTIFIER` constante vive em
`src/constants/universal-identifiers.ts` e é importado onde usado. Os trechos
abaixo omitem essas importações por brevidade — mantenha-as em seus próprios arquivos.
</Note>
## O objeto modelo
Um template tem um `nome`, um `body` com `{{placeholders}}`, e um `alvo` que
diz se é escrito para uma pessoa ou para uma empresa. O `body` é um campo
`RICH_TEXT`, então Vinte dá um editor completo de texto rico.
```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` opção **valores** deve ser `UPPER_CASE` (`PERSON`, não `person`), e o
`defaultValue` está entre aspas extras: `` `'PERSON'` ``. A `etiqueta` é o que
usuários veem.
</Warning>
## O objeto do documento
O documento gerado armazena o `conteúdo` renderizado e um `estado`. Defina
da mesma forma, com uma seleção `status` de `DRAFT` / `GENERATED`. Arquivo completo:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Vinculando eles com uma relação
Cada documento deve apontar para o modelo de onde veio. Relações são
**bidirecionais** — você define ambos os lados, cada um em seu próprio arquivo 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',
},
});
```
O outro lado (`template-documents-relation.field.ts`) é um campo
`RelationType.ONE_TO_MANY` chamado `documentos` que aponta para o lado oposto.
Ver [Relations](/l/pt/developers/extend/apps/data/relations) para o padrão completo.
## Veja em Vinte
Com `yarn 20 dev` executando, abra **Settings → Data model**. Ambos os objetos
aparecem, marcados com seu aplicativo.
<Frame caption="Ambos os objetos personalizados, pertencentes ao aplicativo Gerador de Documentos.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Configurações do modelo de dados mostrando documentos e modelos de documento" />
</Frame>
Crie um modelo para testar com — nomeie-a *proposta de vendas*, defina **destino** para
*Personagem*, e cole um corpo com alguns espaços reservados:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Um registro de template. O corpo mantém seus espaços reservados até que um documento seja gerado.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Um registro de modelo de proposta de vendas com corpo de placeholder" />
</Frame>
**Após essa etapa:** você tem objetos `documentTemplate` e `documento`, ligados por
a relação e um modelo para gerar. Em seguida, a lógica que a preenche.
<Card title="Próximo: gerando documentos →" icon="bolt" href="/desenvolvedores/extend/apps/tutorials/documento-gerador/documentos">
Escreva a função lógica que preenche o template.
</Card>
@@ -0,0 +1,239 @@
---
title: 2. Gerando documentos
icon: bolt
description: Uma função lógica, exposta como uma ferramenta de IA e uma ação de fluxo de trabalho.
---
Agora o núcleo: uma [função lógica](/l/pt/developers/extend/apps/logic/logic-functions)
que carrega um modelo e um registro, preenche os espaços reservados e salva um novo documento
.
Vamos escrever a lógica do negócio uma vez na forma de **manipulador** e então expô-la através de
vários gatilhos. Este capítulo conecta duas delas — uma **ferramenta de IA** e uma
**ação de fluxo de trabalho**.
## O auxiliar de renderização
Mantenha uma lógica pura em seu próprio arquivo, para que seja fácil de testar unidades. Este encolher os tokens de um record
em `{{dot.path}}` e substitui-los.
```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>
Como este arquivo não tem efeitos colaterais, você pode resolvê-lo com testes de unidade
(`yarn test:unit`). Ver [Testing](/l/pt/developers/extend/apps/operations/testing).
</Tip>
## O manipulador
O manipulador usa o [`CoreApiClient`](/l/pt/developers/extend/apps/logic/logic-functions)
para ler e escrever dados de CRM. Ele carrega o modelo, carrega o registro de destino, preenche
o corpo e cria um `documento`.
```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` executa uma consulta diferente para uma Pessoa vs. Uma Empresa e flattens
o resultado — veja
[`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).
## Expo-na como uma ferramenta e uma ação de fluxo de trabalho
Uma única `defineLogicFunction` pode carregar vários gatilhos. Aqui, `toolTriggerSettings`
faz com que seja chamável por agentes IA e `workflowActionTriggerSettings` o transforma em
passo do construtor de fluxo de trabalho. Ambos descrevem suas informações com um 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,
});
```
O esquema de entrada é um esquema JSON simples que descreve `templateId` e `recordId` —
vê [`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).
## Concede-o acesso
Funções lógicas são executadas como o papel do aplicativo. Ele precisa ler templates e registrar
e criar documentos, então permitir que em `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 que a função envie o PDF gerado na próxima seção.
Ver [Roles](/l/pt/developers/extend/apps/config/roles) para permissões refinadas.
## Anexar arquivo PDF real
Um campo de texto renderizado é útil, mas os usuários querem um documento real. Vamos gerar um
**PDF** e armazená-lo no registro como um arquivo para download.
Primeiro, dê ao objeto `documento` um campo `ARQUIVO` para segurar o PDF. Aplicativos enviam
para seus **próprios** campos de arquivos, então este campo é o que encaminha o upload:
```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 },
}
```
Agora renderize esse PDF. Uma app é um projeto Node real, então você pode adicionar qualquer pacote npm
que você precisa e importá-lo como em qualquer outro lugar. Nós usamos **[pdf-lib](https://pdf-lib.js.org/)**
para desenhar o PDF e **[marked](https://marked.js.org/)** para analisar o corpo do Markdown
— a CLI os instala no tempo de execução da função para você:
```bash filename="Terminal"
yarn add pdf-lib marked
```
O auxiliar completo é
[`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).
Ele analisa o Markdown em tokens com `marcado. exer`, então as envia com
pdf-lib: títulos reais, execuções **bold**/*italic*, listas de marcadores e numerados,
bloqueios e regras — uma renderização A4 polida e multi-página do modelo
em si, ao invés de uma parede de texto.
<Frame caption="O PDF: tipografia real e formatação Markdown, renderizando o corpo do modelo.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Um PDF gerado polido e comercializável" />
</Frame>
<Note>
As fontes internas do pdf-lib usam codificação WinAnsi, portanto acentos da Europa Ocidental são renderizados
prontos para uso; o helper mapeia aspas tipográficas e travessões e descarta caracteres que
não consegue codificar. Renderizar scripts não-latinos (chinês, árabe, cirílico) significaria que
incorporando uma fonte Unicode.
</Note>
Em seguida, carregue-a e armazene a referência no registro. Rotas `uploadFile` bytes
para o campo de arquivos de propriedade; o `id` retornado é o que você salva:
```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,
},
});
```
O documento gerado agora carrega um PDF:
<Frame caption="O PDF gerado, armazenado no campo Arquivo do documento.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Um registro de documento com um arquivo PDF gerado" />
</Frame>
<Note>
`uploadFile` destina-se apenas a campos de arquivos **propriedade do aplicativo** (então o upload sempre requer um aplicativo
que possui o campo, mais o sinalizador de papéis `UPLOAD_FILE`). É por isso que o PDF
fica no campo `file` do próprio registro - o mesmo padrão que o
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
usa para gravações.
</Note>
**Após este passo:** cada documento gerado possui um PDF real e para download. Mas
nada pode *chamar* o gerador a partir da interface de usuário — por isso precisamos de uma rota HTTP.
<Card title="Próximo: Rotas HTTP →" icon="globo" href="/desenvolvedores/extend/apps/tutorials/document-gerator/http-routes">
Servir a função sobre HTTP e renderizar documentos como páginas da web.
</Card>
@@ -0,0 +1,147 @@
---
title: 3. Rotas HTTP
icon: globe
description: Acionar a função sobre HTTP e renderizar documentos como páginas da web.
---
O mesmo manipulador também pode responder solicitações HTTP. Vamos adicionar duas rotas:
* um terminal **POST** aponta as chamadas da UI para gerar um documento e
* um endpoint de **GET** público que renderiza um documento como uma página web impressa.
Ambos usam `httpRouteTriggerSettings`. As rotas de aplicativos são servidas em `/s` no seu servidor
Vinte (por exemplo, `http://localhost:2020/s/documents/generate`).
## Rota POST - gerar sob demanda
Isso reusa `generateDocumentHandler`, então não há lógica para repetir — apenas um adaptador
fino que lê o corpo da solicitação.
```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,
},
});
```
O manipulador compartilhado retorna um 'status' sugerido durante a falha, então a rota pode
responder com um código `4xx`/`5xx` apropriado. `isAuthRequired: true` significa que o chamador
deve apresentar um token válido — o componente frontal no próximo capítulo passa o token de acesso do usuário
automaticamente.
## Via GET - renderizar como uma página da web
Para retornar HTML em vez de JSON, encapsule o corpo em um `Response` com um cabeçalho
`Content-Type`. Esta rota é pública (`isAuthRequired: false`) para que um documento
gerado possa ser compartilhado como um link.
```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 o corpo do Markdown para HTML (com [marked](https://marked.js.org/),
sanitizado) e o coloca em um limpo, página imprimível que mostra apenas o conteúdo do modelo- a mesma aparência que o PDF e a visualização no aplicativo.
[Veja o ajudante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Experimente isso
Com um modelo e uma pessoa em seu espaço de trabalho, chame a rota (pegue um token do
**Settings → 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 o documento retornado no seu navegador:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="A rota GET pública renderiza o documento como uma página impressa.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Uma página renderizada do documento web" />
</Frame>
<Tip>
Você também pode transmitir logs de uma função enquanto está testando com
`yarn twenty dev:function:logs`, ou invocá-los diretamente com
`yarn twenty dev:function:exec`.
</Tip>
**Após esse passo:** o aplicativo pode gerar documentos via HTTP e servi-los como páginas
web. Agora vamos torná-lo utilizável sem `curl`.
<Card title="Próximo: Construindo a interface do usuário →" icon="table-columns" href="/desenvolvedores/extend/apps/tutorials/document-generator/building-the-ui">
Exibir, navegação, um comando e um componente inicial.
</Card>
@@ -0,0 +1,70 @@
---
title: "Tutorial: Gerador de Documentos"
icon: wand-magic-sparkles
description: Crie um aplicativo Twenty real que gera documentos personalizados a partir dos seus dados de CRM.
---
Neste tutorial, você vai criar o **Gerador de Documentos** — um aplicativo que transforma modelos reutilizáveis
em documentos personalizados usando os dados que você já tem no seu CRM.
Escreva um modelo uma vez com `{{placeholders}}` e, em seguida, gere um documento preenchido
para qualquer Pessoa ou Empresa com um clique — a partir do menu de comando, de um
agente de IA ou de um workflow.
<Frame caption="Um modelo, gerado para uma pessoa específica, aberto como uma página para impressão.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Um documento de proposta de vendas gerado" />
</Frame>
## O que você vai aprender
Cada capítulo adiciona uma capacidade. Ao final, você terá usado a maior parte do SDK.
| Capítulo | Capacidade | Referência |
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [1. Modelo de Dados](/l/pt/developers/extend/apps/tutorials/document-generator/data-model) | Objetos, campos e uma relação | [Dados](/l/pt/developers/extend/apps/data/overview) |
| [2. Geração de documentos](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents) | Uma função lógica (ferramenta de IA + ação de workflow) que preenche um modelo Markdown e anexa um PDF finalizado | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
| [3. Rotas HTTP](/l/pt/developers/extend/apps/tutorials/document-generator/http-routes) | Servindo JSON e uma página HTML compartilhável a partir de rotas | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
| [4. Criando a UI](/l/pt/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vistas, navegação, menu de comandos e componentes de front-end que preveem um documento e editam um modelo | [Layout](/l/pt/developers/extend/apps/layout/overview) |
| [5. Um agente de IA](/l/pt/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + habilidade | [Habilidades e Agentes](/l/pt/developers/extend/apps/logic/skills-and-agents) |
| [6. Publicação](/l/pt/developers/extend/apps/tutorials/document-generator/publishing) | Envie-o para o marketplace | [Publicação](/l/pt/developers/extend/apps/operations/publishing) |
## Pré-requisitos
Você deve ter concluído o [Quick Start](/l/pt/developers/extend/apps/getting-started/quick-start):
um servidor Twenty local em execução na porta `2020` e o CLI autenticado nele.
Se não, gere a estrutura e inicie um agora:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Prefere ler o código finalizado? O aplicativo completo está em
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Cada trecho abaixo foi copiado de lá.
</Note>
## Como o aplicativo se encaixa
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Um modelo com placeholders é transformado em um documento finalizado com um PDF, acionado a partir do menu de comandos, de um agente de IA, de um workflow ou de um link compartilhável" />
</Frame>
Você escreve um **modelo** uma vez em um editor de rich text, com `{{placeholders}}`. Escolher um
modelo e um registro de CRM preenche os placeholders e armazena um
**documento** finalizado (com um arquivo PDF). Todo o resto — o menu de comandos, o agente de IA,
o passo de workflow, o link compartilhável — é apenas uma forma diferente de acionar aquele
único gerador.
## Mantenha este ciclo em execução
Deixe `yarn twenty dev` rodando em um terminal durante todo o tutorial. Toda vez que
você adicionar ou editar um arquivo em `src/`, ele será ressíncronizado com o seu servidor em alguns
segundos, para que você possa ver cada capacidade aparecer na UI conforme a constrói.
<Card title="Comece a construir →" icon="database" href="/l/pt/developers/extend/apps/tutorials/document-generator/data-model">
Capítulo 1: modele documentos e modelos.
</Card>
@@ -0,0 +1,137 @@
---
title: 6. Publicação
icon: rocket
description: Adicione metadados da loja e publique seu aplicativo.
---
Seu aplicativo funciona. O último passo é descrevê-lo para o mercado e publicar.
## Adicionar metadados de mercado
A [aplicação config](/l/pt/developers/extend/apps/config/application) transporta a identidade
que aparece no mercado: autor, categoria, logotipo e suporte links
. Coloque um logotipo em `public/` e referencie-o com `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/pt/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
A função padrão é declarada com `defineApplicationRole()` em seu próprio arquivo — você
não passa mais `defaultRoleUniversalIdentifier` aqui.
</Tip>
Também adicione a palavra-chave `vinte app` ao `package.json` para que o aplicativo seja detectável:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Adicionar capturas de tela da galeria
Uma lista de mercado vende a si mesma com screenshots. Colocar alguns PNGs em
`public/gallery/` e referenciá-los com `screenshots` — eles renderizam como galeria
na página de listagem.
```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>
Conduzir com o pagamento: fazer a primeira captura de tela o resultado finalizado (um documento
gerado), depois mostrar como é acionado e autorado. Use capturas
crocantes e de alta resolução — elas são a primeira coisa que um usuário vê.
</Tip>
Dê ao `README.md` o mesmo tratamento — é a página inicial em npm e GitHub.
Abra com a proposição de valor e uma captura de tela, liste os recursos do título,
e então mantenha os detalhes de compilação abaixo da dobra.
## Verifique antes de enviar
Executar os mesmos portões CI do portão:
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty dev --once --dry-run # preview the metadata diff
```
A corrida seca imprime exatamente o que mudaria no servidor sem aplicá-lo —
uma boa verificação de sanidade final. Veja
[Testing](/l/pt/developers/extend/apps/operations/testing) e
[Sincronização e recuperação](/l/pt/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` compila e publica para o npm por padrão; `--private` envia uma tarball
para um registro privado de vinte servidores. Para superfíciar um aplicativo publicado
no mercado de uma instância, acione uma sincronização de catálogo:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Detalhes completos e o checklist de lançamento:
[Publishing](/l/pt/developers/extend/apps/operations/publishing).
## Você construiu um aplicativo 🎉
Em seis capítulos você usou a maior parte da superfície SDK:
* **Objetos, campos e uma relação** para modelar os dados
* Uma **função lógica** exposta como uma **ferramenta de Inteligência**, uma **ação de fluxo de trabalho** e **rotas HTTP**
* **Exibir, navegação, um comando e um componente frontal** para a interface do usuário
* Um **agente + habilidade** para a geração de idioma natural
* **Metadados do Marketplace** e o fluxo de publicação
O aplicativo finalizado está em
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Para onde ir
<CardGroup cols={2}>
<Card title="Referência de dados" icon="database" href="/l/pt/developers/extend/apps/data/overview">
Todos os tipos de campo, relação e opção de índice.
</Card>
<Card title="Referência lógica" icon="bolt" href="/l/pt/developers/extend/apps/logic/overview">
Gatilhos Cron e database-event trigers, a loja chave-valor, conexões OAuth
</Card>
<Card title="Referência do layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout/overview">
Layouts da página, widgets do painel e mais superfícies da UI.
</Card>
<Card title="Operações" icon="rocket" href="/l/pt/developers/extend/apps/operations/overview">
CLI, testes, controles remotos e CI.
</Card>
</CardGroup>