Follow-up to #25695, and the platform half of https://github.com/twentyhq/core-team-issues/issues/2895. Not Slack specific: it affects every app that calls `runAgent`. ## Why The in-app chat already gets files to a model. `buildFilePartsFromAttachments` validates ids against `FileFolder.AgentChat`, `loadMessagesFromDB` swaps in a signed URL, and `convertToModelMessages` emits a file part. An app cannot reach that path for one reason: `RunAgentMessage` is `{role, content: string}`, and `agent-async-executor.service.ts` mapped it straight through. So an app could tell an agent a screenshot exists and nothing more. Apps can already write the bytes. `createFileUpload` / `completeFileUpload` are `@MetadataResolver()` mutations, which is where the SDK already sends everything, and `permissions.service.ts` has an application-token branch that resolves the app's default role, so an app declaring `UPLOAD_FILE` can upload into `agent-chat` today. Only the message contract was missing. ## What - `RunAgentMessage` gains an optional `attachments` list of `{fileId, filename?}`, with a matching GraphQL input. - `RunAgentAttachmentService` resolves each fileId against uploaded files in the caller's workspace under `agent-chat`, signs each distinct file once, and emits `{type: 'file', data, mediaType, filename}` parts alongside the text. The shape mirrors what `convertToModelMessages` produces for chat, including for images. - The executor builds its messages through that service. No SDK change was needed: `runAgent` forwards the input object and the mutation does not enumerate input fields. ## Behaviour worth reviewing - **A message with no attachments still reaches the model as a bare string**, not a one-element parts array. This path runs for every app and workflow agent, and widening it must not perturb prompt caching or model behaviour for callers who never asked for files. There is a test pinning this. - **An unresolvable fileId throws** rather than quietly answering without the file. Chat filters silently, but for an API an app calls, a dropped attachment that nobody reports is much harder to debug, and a caller that wants to degrade can catch and retry without it. - **Attachments on an assistant message throw.** They cannot be represented in an assistant model message, so accepting and dropping them would be the same trap. - Capped at 10 attachments per message and 255 characters of filename, enforced in the resolution path. Resolution stays workspace-scoped and folder-scoped, and only `UPLOADED` files resolve, so a fileId referenced before `completeFileUpload` fails here rather than producing a URL that 404s inside the provider. ## A pre-existing gap this surfaced `AgentRunResolver` installs no `ResolverValidationPipe`, so **nothing** on `RunAgentInput` is validated today, including the existing `@ArrayMaxSize(100)` on `messages` and `@IsNotEmpty` on `prompt`. That is why the new limits are enforced in `RunAgentAttachmentService` rather than by decorators alone. Adding the pipe here would switch on every dormant decorator at once, and one of them breaks a shipped caller: `build-slack-conversation-messages.ts` replays thread history, and an assistant turn whose text strips to nothing with no files attached reaches `runAgent` with `content: ''`. That passes today and would start failing `@IsNotEmpty`. Since the Slack app ships separately from the server, an old app build against a new server would break. Worth fixing, with its callers first, but not as a side effect of this PR. ## Testing - 9 unit tests on the resolver service; full `ai-agent-execution` and `ai` module suites pass. - `tsgo --noEmit` clean on twenty-server, twenty-shared and twenty-sdk; oxlint and oxfmt clean. - Metadata GraphQL artifacts regenerated against a locally running server. The schema diff is the new input type; the churn in the client SDK `types.ts` is index renumbering from inserting it. Not verified end to end: that an image actually lands in front of a model. That needs a live run against a real provider. The message shape matches what chat already sends successfully, but that is inference rather than observation. ## Next The Slack consumer is a separate app-package change: declare `UPLOAD_FILE`, add the `files:read` bot scope (which forces existing installations to reauthorise), download from `url_private` server-side, upload, and pass the fileIds here. The names-only path from #25695 stays as the fallback.
Twenty Documentation
Official documentation for Twenty CRM, powered by Mintlify.
🌐 Live Site
Visit the documentation at docs.twenty.com
📚 Content
This repository contains:
- User Guide (46 pages) - Complete guide for Twenty users
- Developers (24 pages) - Technical documentation for developers
- UI Library - Guides and component references
🚀 Local Development
To run the documentation locally:
# From the twenty monorepo root
npx nx run twenty-docs:dev
The documentation will be available at http://localhost:3000
📝 Editing Content
Adding/Editing Pages
-
Edit MDX files in the appropriate directory:
user-guide/- User documentationdevelopers/- Developer documentationui/- UI library guides and component references
-
Update
navigation/base-structure.jsonif you need to change the tab/group hierarchy or add/remove pages. This file stays in the repo and is not uploaded to Crowdin. -
Keep the translation template (
navigation/navigation.template.json) in sync by runningyarn docs:generate-navigation-templateafter editing the base structure. This template is the only file that should be pushed to Crowdin. -
For each translated locale pulled from Crowdin, ensure a
packages/twenty-docs/l/<language>/navigation.jsonfile exists. These files contain labels only; page slugs always come from the base structure. -
Run
yarn docs:generateto rebuilddocs.jsonfrom the base structure + translated labels.
MDX Format
All documentation pages use MDX format with frontmatter:
---
title: Page Title
description: Page description
image: /images/path/to/image.png
---
Your content here...
Adding Images
- Place images in the
/images/directory - Reference them in MDX:
 - Or use Mintlify Frame component:
<Frame>
<img src="/images/your-image.png" alt="Description" />
</Frame>
🔧 Configuration
navigation/base-structure.json- Source of truth for tabs, groups, icons, and page slugs (English source labels, not sent to Crowdin).navigation/navigation.template.json- Generated translation template (labels only) that is uploaded to Crowdin.l/<language>/navigation.json- Locale-specific label files pulled from Crowdin.docs.json- Generated Mintlify configuration (always runyarn docs:generateafter modifying navigation files).package.json- Package dependencies and scripts (docs:generate,docs:generate-navigation-template, …).project.json- Nx workspace configuration
📦 Validation
# Validate the documentation build
npx nx run twenty-docs:validate
UI reference generation
Run npx nx generate:ui twenty-docs after changing documented component props or theme tokens. Run npx nx check:ui twenty-docs to check generated references and compile the UI guide examples against the public entry points. Hand-written pages live under ui/; generated snippets live under snippets/ui/generated/.
UI guides, component pages, and navigation labels use the existing Crowdin translation workflow. Translated pages appear in their locale’s navigation once the corresponding files exist. Generated API and token snippets are shared across locales and maintained by the reference generator.
🔗 Links
🤝 Contributing
To contribute to the documentation:
- Fork the repository
- Make your changes in the
packages/twenty-docsdirectory - Test locally with
npx nx run twenty-docs:dev - Submit a pull request
📄 License
This documentation is part of the Twenty project and is licensed under AGPL-3.0.