Files
Abdul Rahman ef6afd5a20 Let runAgent messages carry file attachments (#25835)
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.
2026-09-15 15:36:27 +00:00
..

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

  1. Edit MDX files in the appropriate directory:

    • user-guide/ - User documentation
    • developers/ - Developer documentation
    • ui/ - UI library guides and component references
  2. Update navigation/base-structure.json if you need to change the tab/group hierarchy or add/remove pages. This file stays in the repo and is not uploaded to Crowdin.

  3. Keep the translation template (navigation/navigation.template.json) in sync by running yarn docs:generate-navigation-template after editing the base structure. This template is the only file that should be pushed to Crowdin.

  4. For each translated locale pulled from Crowdin, ensure a packages/twenty-docs/l/<language>/navigation.json file exists. These files contain labels only; page slugs always come from the base structure.

  5. Run yarn docs:generate to rebuild docs.json from 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

  1. Place images in the /images/ directory
  2. Reference them in MDX: ![Alt text](/images/your-image.png)
  3. 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 run yarn docs:generate after 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 locales navigation once the corresponding files exist. Generated API and token snippets are shared across locales and maintained by the reference generator.

🤝 Contributing

To contribute to the documentation:

  1. Fork the repository
  2. Make your changes in the packages/twenty-docs directory
  3. Test locally with npx nx run twenty-docs:dev
  4. Submit a pull request

📄 License

This documentation is part of the Twenty project and is licensed under AGPL-3.0.