# Metadata Structure by Content Type

## Content: Metadata Structure by Content Type

### User Intent

"What metadata is captured for each content type? How do I access it?"

### Operation

* **Concept**: Content metadata fields
* **GraphQL Fields**: Type-specific metadata objects
* **Entity Type**: Content
* **Common Use Cases**: Accessing email details, message info, document properties, image metadata, event details

### Metadata Fields Overview

Each ContentType has a corresponding metadata field with type-specific properties automatically captured during ingestion.

### TypeScript (Canonical)

```typescript
import { Graphlit } from 'graphlit-client';
import { ContentTypes, FileTypes } from 'graphlit-client/dist/generated/graphql-types';

const graphlit = new Graphlit();

const content = await graphlit.getContent('content-id');

// Access metadata based on content type
if (content.content.type === ContentTypes.Email && content.content.email) {
  const email = content.content.email;
  console.log(`Subject: ${email.subject}`);
  console.log(`From: ${email.from[0].email}`);
  console.log(`To: ${email.to?.map(p => p.email).join(', ')}`);
  console.log(`Labels: ${email.labels?.join(', ')}`);
  console.log(`Attachments: ${email.attachmentCount}`);
}

if (content.content.type === ContentTypes.Message && content.content.message) {
  const msg = content.content.message;
  console.log(`Channel: ${msg.channelName}`);
  console.log(`Author: ${msg.author?.name} (${msg.author?.email})`);
  console.log(`Mentions: ${msg.mentions?.map(p => p.name).join(', ')}`);
}

if (content.content.fileType === FileTypes.Document && content.content.document) {
  const doc = content.content.document;
  console.log(`Pages: ${doc.pageCount}`);
  console.log(`Author: ${doc.author}`);
  console.log(`Words: ${doc.wordCount}`);
  console.log(`Encrypted: ${doc.isEncrypted}`);
}
```

### Email Metadata (ContentTypes.Email)

**Field**: `content.email` (EmailMetadata)

**Properties**:

```typescript
interface EmailMetadata {
  identifier: string;              // Message-ID
  threadIdentifier: string;        // In-Reply-To / References
  subject: string;
  from: PersonReference[];         // Sender
  to: PersonReference[];           // Recipients
  cc: PersonReference[];           // CC recipients
  bcc: PersonReference[];          // BCC recipients
  labels: string[];                // Gmail labels or folder names
  sensitivity: MailSensitivity;    // NORMAL, PERSONAL, PRIVATE, CONFIDENTIAL
  priority: MailPriority;          // LOW, NORMAL, HIGH
  importance: MailImportance;      // LOW, NORMAL, HIGH
  attachmentCount: number;
  unsubscribeUrl: string;          // Newsletter unsubscribe
  publicationName: string;         // Newsletter name
  publicationUrl: string;          // Newsletter URL
}

interface PersonReference {
  name: string;
  email: string;
  givenName: string;
  familyName: string;
}
```

**Example Access**:

```typescript
const email = content.content.email;

// Sender details
console.log(`From: ${email.from[0].name} <${email.from[0].email}>`);

// All recipients
const allRecipients = [
  ...email.to || [],
  ...email.cc || [],
  ...email.bcc || []
];
console.log(`Total recipients: ${allRecipients.length}`);

// Thread detection
if (email.threadIdentifier) {
  console.log(`Part of thread: ${email.threadIdentifier}`);
}

// Newsletter detection
if (email.publicationName) {
  console.log(`Newsletter: ${email.publicationName}`);
}
```

### Message Metadata (ContentTypes.Message)

**Field**: `content.message` (MessageMetadata)

**Properties**:

```typescript
interface MessageMetadata {
  identifier: string;              // Message ID
  conversationIdentifier: string;  // Thread/conversation ID
  channelIdentifier: string;       // Channel ID
  channelName: string;             // Channel name (e.g., "engineering")
  author: PersonReference;         // Message author
  mentions: PersonReference[];     // @mentioned users
  attachmentCount: number;
  links: string[];                 // URLs in message
}
```

**Example Access**:

```typescript
const msg = content.content.message;

// Author info
console.log(`Posted by: ${msg.author.name} in #${msg.channelName}`);

// Mentions
if (msg.mentions && msg.mentions.length > 0) {
  console.log(`Mentioned: ${msg.mentions.map(p => '@' + p.name).join(' ')}`);
}

// Links shared
if (msg.links && msg.links.length > 0) {
  console.log(`Links: ${msg.links.length}`);
}

// Thread detection
if (msg.conversationIdentifier !== msg.identifier) {
  console.log(`Reply in conversation: ${msg.conversationIdentifier}`);
}
```

### Document Metadata (FileDocument)

**Field**: `content.document` (DocumentMetadata)

**Properties**:

```typescript
interface DocumentMetadata {
  title: string;
  subject: string;
  summary: string;
  author: string;
  lastModifiedBy: string;
  publisher: string;
  description: string;
  keywords: string[];
  pageCount: number;
  worksheetCount: number;          // Excel
  slideCount: number;              // PowerPoint
  wordCount: number;
  lineCount: number;
  paragraphCount: number;
  isEncrypted: boolean;
  hasDigitalSignature: boolean;
}
```

**Example Access**:

```typescript
const doc = content.content.document;

// Document stats
console.log(`${doc.pageCount} pages, ${doc.wordCount} words`);
console.log(`Author: ${doc.author}`);

// Excel-specific
if (doc.worksheetCount) {
  console.log(`Excel workbook: ${doc.worksheetCount} worksheets`);
}

// PowerPoint-specific
if (doc.slideCount) {
  console.log(`Presentation: ${doc.slideCount} slides`);
}

// Security
if (doc.isEncrypted) {
  console.log(` Encrypted document`);
}
if (doc.hasDigitalSignature) {
  console.log(`✓ Digitally signed`);
}
```

### Image Metadata (FileImage)

**Field**: `content.image` (ImageMetadata)

**Properties**:

```typescript
interface ImageMetadata {
  width: number;
  height: number;
  resolutionX: number;             // DPI horizontal
  resolutionY: number;             // DPI vertical
  bitsPerComponent: number;
  components: number;
  projectionType: ImageProjectionTypes;  // EQUIRECTANGULAR, etc.
  orientation: OrientationTypes;
  description: string;
  
  // Camera EXIF data
  make: string;                    // Camera manufacturer
  model: string;                   // Camera model
  software: string;                // Editing software
  lens: string;
  focalLength: number;             // mm
  exposureTime: string;            // e.g., "1/125"
  fNumber: string;                 // e.g., "f/2.8"
  iso: string;                     // e.g., "ISO 400"
  
  // GPS data
  heading: number;                 // Compass direction
  pitch: number;                   // Angle
}
```

**Example Access**:

```typescript
const img = content.content.image;

// Basic info
console.log(`${img.width}x${img.height} pixels`);
console.log(`Resolution: ${img.resolutionX} DPI`);

// Camera info
if (img.make && img.model) {
  console.log(`Camera: ${img.make} ${img.model}`);
  console.log(`Lens: ${img.lens}`);
  console.log(`Settings: ${img.exposureTime} at ${img.fNumber}, ${img.iso}`);
}

// GPS location
if (content.content.location) {
  console.log(`Location: ${content.content.location.latitude}, ${content.content.location.longitude}`);
}
```

### Audio Metadata (FileAudio)

**Field**: `content.audio` (AudioMetadata)

**Properties**:

```typescript
interface AudioMetadata {
  title: string;
  description: string;
  author: string;
  keywords: string[];
  
  // Podcast-specific
  series: string;                  // Podcast series name
  episode: string;                 // Episode number
  episodeType: string;             // FULL, TRAILER, BONUS
  season: string;                  // Season number
  publisher: string;
  copyright: string;
  genre: string;
  
  // Technical
  bitrate: number;                 // bits per second
  channels: number;                // 1=mono, 2=stereo
  sampleRate: number;              // Hz
  bitsPerSample: number;
  duration: string;                // ISO 8601 duration
}
```

**Example Access**:

```typescript
const audio = content.content.audio;

// Podcast info
if (audio.series) {
  console.log(`Podcast: ${audio.series}`);
  console.log(`Episode: ${audio.episode} (${audio.episodeType})`);
  if (audio.season) console.log(`Season: ${audio.season}`);
}

// Audio quality
console.log(`${audio.bitrate / 1000}kbps, ${audio.sampleRate}Hz`);
console.log(`${audio.channels === 1 ? 'Mono' : 'Stereo'}`);

// Duration
import { Duration } from 'luxon';
const dur = Duration.fromISO(audio.duration);
console.log(`Length: ${dur.toFormat('mm:ss')}`);
```

### Video Metadata (FileVideo)

**Field**: `content.video` (VideoMetadata)

**Properties**:

```typescript
interface VideoMetadata {
  width: number;
  height: number;
  duration: string;                // ISO 8601 duration
  title: string;
  description: string;
  keywords: string[];
  author: string;
  
  // Camera/device info
  make: string;
  model: string;
  software: string;
}
```

**Example Access**:

```typescript
const video = content.content.video;

console.log(`${video.width}x${video.height}`);
console.log(`Duration: ${video.duration}`);
console.log(`Title: ${video.title}`);

if (video.make) {
  console.log(`Recorded on: ${video.make} ${video.model}`);
}
```

### Event Metadata (ContentEvent)

**Field**: `content.event` (EventMetadata)

**Properties**:

```typescript
interface EventMetadata {
  eventIdentifier: string;
  calendarIdentifier: string;
  subject: string;
  startDateTime: Date;
  endDateTime: Date;
  isAllDay: boolean;
  timezone: string;
  status: CalendarEventStatus;         // CONFIRMED, TENTATIVE, CANCELLED
  visibility: CalendarEventVisibility; // PUBLIC, PRIVATE, CONFIDENTIAL
  meetingLink: string;
  categories: string[];
  
  organizer: CalendarAttendee;
  attendees: CalendarAttendee[];
  reminders: CalendarReminder[];
  
  // Recurring events
  recurringEventIdentifier: string;
  isRecurring: boolean;
  recurrence: CalendarRecurrence;
}

interface CalendarAttendee {
  name: string;
  email: string;
  isOptional: boolean;
  isOrganizer: boolean;
  responseStatus: CalendarAttendeeResponseStatus;  // ACCEPTED, DECLINED, TENTATIVE, NEEDS_ACTION
}
```

**Example Access**:

```typescript
const event = content.content.event;

console.log(`Event: ${event.subject}`);
console.log(`When: ${event.startDateTime} - ${event.endDateTime}`);
console.log(`Timezone: ${event.timezone}`);

// Organizer
console.log(`Organizer: ${event.organizer.name} <${event.organizer.email}>`);

// Attendees
console.log(`Attendees (${event.attendees?.length || 0}):`);
event.attendees?.forEach(att => {
  const status = att.responseStatus;
  const icon = att.isOptional ? '(optional)' : '(required)';
  console.log(`  ${att.name}: ${status} ${icon}`);
});

// Meeting link
if (event.meetingLink) {
  console.log(`Join: ${event.meetingLink}`);
}

// Recurring
if (event.isRecurring) {
  console.log(`Recurring: ${event.recurrence.pattern} every ${event.recurrence.interval}`);
}
```

### Issue Metadata (ContentTypes.Issue)

**Field**: `content.issue` (IssueMetadata)

**Properties**:

```typescript
interface IssueMetadata {
  identifier: string;              // Issue ID/key (e.g., "PROJ-123")
  title: string;
  project: string;                 // Project name
  team: string;                    // Team name (Linear)
  status: string;                  // Status name
  priority: string;                // Priority level
  type: string;                    // Issue type (Bug, Feature, etc.)
  labels: string[];
}
```

**Example Access**:

```typescript
const issue = content.content.issue;

console.log(`${issue.identifier}: ${issue.title}`);
console.log(`Project: ${issue.project}`);
console.log(`Status: ${issue.status}`);
console.log(`Priority: ${issue.priority}`);
console.log(`Type: ${issue.type}`);

if (issue.labels && issue.labels.length > 0) {
  console.log(`Labels: ${issue.labels.join(', ')}`);
}
```

### Post Metadata (ContentPost)

**Field**: `content.post` (PostMetadata)

**Properties**:

```typescript
interface PostMetadata {
  identifier: string;              // Post ID
  title: string;
  author: PersonReference;
  upvotes: number;
  downvotes: number;
  commentCount: number;
  links: string[];
}
```

**Example Access**:

```typescript
const post = content.content.post;

console.log(`${post.title}`);
console.log(`By: ${post.author.name}`);
console.log(`Score: ${post.upvotes - post.downvotes} (${post.upvotes}↑ ${post.downvotes}↓)`);
console.log(`Comments: ${post.commentCount}`);

if (post.links && post.links.length > 0) {
  console.log(`Links: ${post.links.join(', ')}`);
}
```

## Access email metadata (snake\_case fields)

if content.content.type == "EMAIL" and content.content.email: email = content.content.email print(f"Subject: {email.subject}") print(f"From: {email.from\_\[0].email}") # Note: from\_ (reserved word) print(f"Attachments: {email.attachment\_count}")

## Access message metadata

if content.content.type == "MESSAGE" and content.content.message: msg = content.content.message print(f"Channel: {msg.channel\_name}") print(f"Author: {msg.author.name}")

## Access document metadata

if content.content.file\_type == "DOCUMENT" and content.content.document: doc = content.content.document print(f"Pages: {doc.page\_count}") print(f"Words: {doc.word\_count}")

````

**C#**:
```csharp
using Graphlit;

var client = new Graphlit();

var content = await graphlit.GetContent("content-id");

// Access email metadata (PascalCase)
if (content.Content.Type == ContentTypes.Email && content.Content.Email != null)
{
    var email = content.Content.Email;
    Console.WriteLine($"Subject: {email.Subject}");
    Console.WriteLine($"From: {email.From[0].Email}");
    Console.WriteLine($"Attachments: {email.AttachmentCount}");
}

// Access message metadata
if (content.Content.Type == ContentTypes.Message && content.Content.Message != null)
{
    var msg = content.Content.Message;
    Console.WriteLine($"Channel: {msg.ChannelName}");
    Console.WriteLine($"Author: {msg.Author.Name}");
}
````

### Developer Hints

#### Always Check Type First

```typescript
//  WRONG - Might be null
console.log(content.content.email.subject);

//  CORRECT - Check type first
if (content.content.type === ContentTypes.Email && content.content.email) {
  console.log(content.content.email.subject);
}
```

#### Null-Safe Access

```typescript
// Use optional chaining for nested properties
console.log(`Attachments: ${content.content.email?.attachmentCount || 0}`);
console.log(`Mentions: ${content.content.message?.mentions?.length || 0}`);
```

#### PersonReference Pattern

```typescript
// PersonReference appears in multiple metadata types
function formatPerson(person: PersonReference): string {
  if (person.name && person.email) {
    return `${person.name} <${person.email}>`;
  }
  return person.name || person.email || 'Unknown';
}

// Use for email, message mentions, event attendees, etc.
console.log(formatPerson(content.content.email.from[0]));
```

### Common Issues & Solutions

**Issue**: Metadata field is undefined **Solution**: Check content type matches expected type

```typescript
// Check before accessing
if (content.content.type === ContentTypes.Email && content.content.email) {
  // Safe to access email metadata
}
```

**Issue**: Arrays are null instead of empty **Solution**: Use nullish coalescing

```typescript
const labels = content.content.email?.labels || [];
const mentions = content.content.message?.mentions || [];
```

**Issue**: Date fields are strings not Date objects **Solution**: Parse ISO 8601 strings

```typescript
const created = new Date(content.content.creationDate);
const eventStart = new Date(content.content.event.startDateTime);
```

### Production Example

```typescript
async function analyzeContent(contentId: string) {
  const content = await graphlit.getContent(contentId);
  
  console.log(`\n=== CONTENT ANALYSIS ===`);
  console.log(`ID: ${content.content.id}`);
  console.log(`Name: ${content.content.name}`);
  console.log(`Type: ${content.content.type}`);
  console.log(`Created: ${content.content.creationDate}`);
  
  // Type-specific metadata
  switch (content.content.type) {
    case ContentTypes.Email:
      if (content.content.email) {
        console.log(`\n Email Metadata:`);
        console.log(`  From: ${content.content.email.from[0].email}`);
        console.log(`  Subject: ${content.content.email.subject}`);
        console.log(`  Recipients: ${content.content.email.to?.length || 0}`);
        console.log(`  Labels: ${content.content.email.labels?.join(', ') || 'none'}`);
      }
      break;
      
    case ContentTypes.Message:
      if (content.content.message) {
        console.log(`\n💬 Message Metadata:`);
        console.log(`  Channel: ${content.content.message.channelName}`);
        console.log(`  Author: ${content.content.message.author?.name}`);
        console.log(`  Mentions: ${content.content.message.mentions?.length || 0}`);
      }
      break;
      
    case ContentTypes.File:
      if (content.content.fileType === FileTypes.Document && content.content.document) {
        console.log(`\n Document Metadata:`);
        console.log(`  Pages: ${content.content.document.pageCount}`);
        console.log(`  Words: ${content.content.document.wordCount}`);
        console.log(`  Author: ${content.content.document.author || 'Unknown'}`);
      } else if (content.content.fileType === FileTypes.Image && content.content.image) {
        console.log(`\n🖼 Image Metadata:`);
        console.log(`  Size: ${content.content.image.width}x${content.content.image.height}`);
        console.log(`  Camera: ${content.content.image.make} ${content.content.image.model}`);
      }
      break;
  }
  
  // Common properties
  console.log(`\nFile: ${content.content.fileName || 'N/A'}`);
  console.log(`Size: ${content.content.fileSize || 0} bytes`);
  console.log(`MIME: ${content.content.mimeType || 'N/A'}`);
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.graphlit.dev/api-guides/use-cases/content/content-metadata-structure.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
