> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Timeline Activity Types

> Define automatic audit events and explicit app events that render in record timelines.

A timeline activity type defines the stable vocabulary for an event shown in a record's timeline. Standard objects and app objects use the same contract: a type has a label and icon, can optionally declare when it is emitted, and can optionally render through one of your app's [front components](/developers/extend/apps/layout/front-components).

<Note>
  Timeline activity types are in beta and are coming soon in Twenty 2.34. The
  API can evolve while we learn from app developers' use cases.
</Note>

Create one with the scaffolder:

```bash filename="Terminal" theme={null}
yarn twenty dev:add timelineActivityType
```

Or define it directly:

```ts filename="src/timeline-activity-types/post-card-created.ts" theme={null}
import { defineTimelineActivityType } from 'twenty-sdk/define';

import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';

export default defineTimelineActivityType({
  universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
  name: 'postCardCreated',
  label: 'created a post card',
  icon: 'IconMail',
  emit: {
    on: 'created',
    objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  },
});
```

## Automatic and explicit events

Add `emit` when Twenty should create this type automatically. `emit.on` supports `created`, `updated`, `deleted`, `restored`, `linked`, and `unlinked`, while `emit.objectUniversalIdentifier` identifies the source object. Only one effective type can handle the same emit key—`on`, object, and optional through relation—in a workspace.

Without `emit.through`, the event is written on the source record's own timeline. To fan it out to related records, set `emit.through.relationFieldUniversalIdentifier` to either a direct many-to-one relation or a [one-to-many junction relation](/developers/extend/apps/data/relations#junction-relations) on the source object. Morph relations fan out to every member of their morph group, so a single declaration can target several object types.

For a direct relation, creating or restoring the source record produces `linked`, deleting it produces `unlinked`, and repointing the relation produces `unlinked` on the previous target plus `linked` on the new target. Other source updates do not produce link events. This is the contract used by attachments, whose target is a direct morph relation.

For a junction relation, `universalSettings.junctionTargetFieldUniversalIdentifier` identifies the relation from the junction object to the target. Creating or deleting a junction row produces `linked` or `unlinked`. Repointing either side of a junction row also produces a link event; updates to unrelated junction fields do not.

`linked` and `unlinked` events require `emit.through`, because their trigger is a change to the configured direct or junction relation.

For example, this is the same generic contract used by notes, tasks, messages, and calendar events:

```ts filename="src/timeline-activity-types/post-card-linked.ts" theme={null}
import { defineTimelineActivityType } from 'twenty-sdk/define';

import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';

export default defineTimelineActivityType({
  universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
  name: 'postCardLinked',
  label: 'received a post card',
  icon: 'IconMail',
  emit: {
    on: 'linked',
    objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
    through: {
      relationFieldUniversalIdentifier:
        POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
    },
  },
});
```

An `updated` through event is emitted for every source update by default. Set `emit.through.triggerFieldUniversalIdentifiers` when only changes to selected source fields should appear on the target timelines.

Omit `emit` for an explicit domain event that your logic function creates itself. This avoids producing both an automatic audit row and an explicit row for the same operation.

```ts filename="src/timeline-activity-types/post-card-sent.ts" theme={null}
import { defineTimelineActivityType } from 'twenty-sdk/define';

export default defineTimelineActivityType({
  universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
  name: 'postCardSent',
  label: 'sent a post card',
  icon: 'IconSend',
});
```

Create explicit events with [`createTimelineActivity()`](/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). App code uses stable universal identifiers; Twenty resolves the installation-specific metadata IDs.

## Custom rendering

Without a front component, Twenty renders a native generic row from the type label, icon, and linked-object metadata. This works for standard and custom objects without an object-specific renderer.

For custom details, set `frontComponentUniversalIdentifier` to a front component owned by the same app:

```ts theme={null}
export default defineTimelineActivityType({
  universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
  name: 'postCardSent',
  label: 'sent a post card',
  icon: 'IconSend',
  frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```

The native row remains the collapsed presentation. Twenty mounts the front component only after the user expands that row, avoiding a sandbox and worker for every visible event. Inside the component, call `useTimelineActivityId()` from `twenty-sdk/front-component` to read the row ID and fetch any data your presentation needs. It returns `null` when the component is rendered outside a timeline row.

## Overriding another application's presentation

An app owns automatic timeline contracts for its own objects. To customize an event on an object owned by another app, declare the existing type explicitly with `replacesTimelineActivityTypeUniversalIdentifier`:

```ts theme={null}
export default defineTimelineActivityType({
  universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
  name: 'companyCreatedWithDeploymentContext',
  label: 'was created from a deployment',
  emit: {
    on: 'created',
    objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
  },
  replacesTimelineActivityTypeUniversalIdentifier:
    RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
  frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```

An override must include `emit`, because it replaces an existing automatic emit slot rather than an explicit-only type. The referenced type must belong to the target object's application and describe the same action and route. Removing the overriding app restores the base type; removing the base type disables the override.

## Workspace overrides and muting

Workspace administrators can change a type's presentation or mute its automatic and explicit events without forking the application. Use the metadata API's `updateTimelineActivityType` mutation with the installation-specific type ID:

```graphql theme={null}
mutation CustomizeTimelineActivityType(
  $id: UUID!
  $label: String
  $icon: String
  $isActive: Boolean
) {
  updateTimelineActivityType(
    input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
  ) {
    id
    label
    icon
    isActive
  }
}
```

`label` and `icon` are stored as workspace overrides, so later application updates do not overwrite the administrator's choices. Set `isActive` to `false` to stop automatic emission and reject new explicit events of that type. Existing rows remain visible.

Restore the application defaults, including the active state, with:

```graphql theme={null}
mutation ResetTimelineActivityType($id: UUID!) {
  resetTimelineActivityType(id: $id) {
    id
    label
    icon
    isActive
  }
}
```

## Configuration fields

| Field                                             | Required  | Description                                                      |
| ------------------------------------------------- | --------- | ---------------------------------------------------------------- |
| `universalIdentifier`                             | Yes       | Stable UUID for the type across installations and upgrades       |
| `name`                                            | Yes       | App-local programmatic name                                      |
| `label`                                           | Yes       | User-facing action text used by the native renderer              |
| `icon`                                            | No        | Twenty icon name displayed beside the event                      |
| `emit`                                            | No        | Automatic emission declaration; omit for explicit-only types     |
| `emit.on`                                         | With emit | Audit action that causes this type to be written                 |
| `emit.objectUniversalIdentifier`                  | With emit | Object whose records emit this type                              |
| `emit.through.relationFieldUniversalIdentifier`   | No        | Direct or junction relation used to fan out to related timelines |
| `emit.through.triggerFieldUniversalIdentifiers`   | No        | Source fields that can trigger an `updated` through event        |
| `frontComponentUniversalIdentifier`               | No        | App-owned front component mounted when the row is expanded       |
| `replacesTimelineActivityTypeUniversalIdentifier` | No        | Existing emit type replaced; requires `emit`                     |

Twenty snapshots semantic identity and durable presentation fallbacks when an event is created. Historical rows keep their original action and object meaning. While the type is installed, its current translated label, icon, and front component render live; after uninstall, the snapshot fallback keeps the row readable. Resolution uses the universal identifier, so presentation also survives reinstalling the app.
