Coline Docs
SDK

Examples

Code examples and recipes for common tasks.

SDK Examples

Common patterns and recipes for building with the Coline SDK.

File Management

Upload and Share a File

import { ColineApiClient } from '@colineapp/sdk'

const client = new ColineApiClient({
  baseUrl: 'https://api.coline.app',
  apiKey: process.env.COLINE_API_KEY
})

const ws = client.workspace('ws_abc123')

async function uploadAndShare(fileBuffer: Buffer, fileName: string) {
  // Upload file
  const result = await ws.uploadFile({
    data: new Blob([fileBuffer]),
    fileName,
    mimeType: 'application/pdf',
    driveId: 'drive_shared'
  })
  
  console.log(`File uploaded: ${result.file.id}`)
  
  // Send to channel with mention
  await ws.sendChannelMessage('channel_general', {
    content: Message
      .text('📄 New file uploaded: ')
      .mention('file', result.file.name)
      .toInput()
  })
  
  return result.file
}

Sync Files to External Storage

async function syncFilesToS3(driveId: string) {
  // Paginate through all files
  for await (const file of paginateFiles(ws, driveId)) {
    // Download and upload to S3
    const content = await downloadFile(file.id)
    await uploadToS3(file.name, content)
    
    // Log progress
    console.log(`Synced: ${file.name}`)
  }
}

async function* paginateFiles(ws: ColineWorkspace, driveId: string) {
  let cursor: string | undefined
  
  do {
    const result = await ws.listDriveFiles(driveId, {
      limit: 100,
      cursor
    })
    
    for (const file of result.files) {
      yield file
    }
    
    cursor = result.page.nextCursor ?? undefined
  } while (cursor)
}

Notes & Docs

Create Daily Standup Notes

async function createStandupNotes(workspaceSlug: string, userName: string) {
  const today = new Date().toISOString().split('T')[0]
  
  const note = await ws.createNote({
    title: `Standup Notes - ${today}`,
    body: [
      `# Daily Standup - ${today}`,
      '',
      '## Yesterday',
      '- ',
      '',
      '## Today',
      '- ',
      '',
      '## Blockers',
      '- None'
    ].join('\n'),
    driveId: 'drive_personal'
  })
  
  // Share in standup channel
  await ws.sendChannelMessage('channel_standup', {
    content: Message
      .text(`Standup notes for ${today}: `)
      .mention('file', note.title)
      .toInput()
  })
  
  return note
}

Generate Report from Notes

async function generateReport(query: string) {
  // Search for relevant notes
  const notes = await ws.paginateNotes({ q: query })
  
  const sections: string[] = []
  
  for await (const note of notes) {
    const detail = await ws.getNote(note.id)
    sections.push(`## ${detail.title}\n\n${detail.body}`)
  }
  
  // Create compiled doc
  const doc = await ws.createDoc({
    title: `Report: ${query}`,
    content: {
      blocks: sections.map(text => ({
        type: 'paragraph',
        content: text
      }))
    },
    driveId: 'drive_reports'
  })
  
  return doc
}

Messaging

Weekly Channel Summary

async function weeklySummary(channelId: string) {
  // Get messages from last 7 days
  const oneWeekAgo = new Date()
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7)
  
  const messages = await ws.listChannelMessages(channelId, { limit: 100 })
  
  // Extract action items (messages with "action item" or todo format)
  const actionItems = messages.messages.filter(msg => 
    msg.plaintext.toLowerCase().includes('action item') ||
    msg.plaintext.includes('- [ ]')
  )
  
  // Create summary note
  const summary = await ws.createNote({
    title: `Weekly Summary - ${new Date().toLocaleDateString()}`,
    body: [
      '# Weekly Channel Summary',
      '',
      `## Action Items (${actionItems.length})`,
      ...actionItems.map(m => `- ${m.plaintext}`),
      '',
      `## Total Messages: ${messages.messages.length}`
    ].join('\n'),
    driveId: 'drive_summaries'
  })
  
  return summary
}

Bot That Responds to Commands

async function botResponder(channelId: string) {
  // Get recent messages
  const result = await ws.listChannelMessages(channelId, { limit: 20 })
  
  for (const msg of result.messages) {
    // Skip own messages
    if (msg.authorUserId === BOT_USER_ID) continue
    
    // Check for commands
    if (msg.plaintext.startsWith('!status')) {
      await ws.sendChannelMessage(channelId, {
        content: Message
          .text('✅ Bot is online and ready!')
          .toInput()
      })
    }
    
    if (msg.plaintext.startsWith('!tasks')) {
      const tasks = await ws.listTasks('taskboard_main', { limit: 5 })
      const taskList = tasks.tasks.map(t => `• ${t.title}`).join('\n')
      
      await ws.sendChannelMessage(channelId, {
        content: Message
          .text(`📋 Top 5 tasks:\n${taskList}`)
          .toInput()
      })
    }
  }
}

Tasks & Project Management

Sprint Report

async function generateSprintReport(taskboardId: string) {
  const tasks = await ws.listTasks(taskboardId)
  
  // Group by status
  const byStatus: Record<string, number> = {}
  const byAssignee: Record<string, number> = {}
  
  for (const task of tasks.tasks) {
    byStatus[task.statusId] = (byStatus[task.statusId] || 0) + 1
    
    for (const assignee of task.assignees) {
      byAssignee[assignee.userId] = (byAssignee[assignee.userId] || 0) + 1
    }
  }
  
  // Create report doc
  const report = await ws.createDoc({
    title: `Sprint Report - ${new Date().toLocaleDateString()}`,
    content: {
      blocks: [
        {
          type: 'paragraph',
          content: '# Sprint Status Report'
        },
        {
          type: 'paragraph',
          content: `## By Status\n${Object.entries(byStatus)
            .map(([status, count]) => `- ${status}: ${count}`)
            .join('\n')}`
        },
        {
          type: 'paragraph',
          content: `## Workload Distribution\n${Object.entries(byAssignee)
            .map(([user, count]) => `- ${user}: ${count} tasks`)
            .join('\n')}`
        }
      ]
    },
    driveId: 'drive_reports'
  })
  
  return report
}

Create Tasks from Messages

async function createTasksFromMessages(channelId: string, taskboardId: string) {
  const messages = await ws.listChannelMessages(channelId, { limit: 50 })
  
  const createdTasks: string[] = []
  
  for (const msg of messages.messages) {
    // Look for messages starting with "TODO:"
    if (msg.plaintext.toLowerCase().startsWith('todo:')) {
      const title = msg.plaintext.replace(/^todo:/i, '').trim()
      
      const task = await ws.createTask(taskboardId, {
        title,
        description: `From message by ${msg.authorUserId}`,
        priority: 'medium'
      })
      
      createdTasks.push(task.task.id)
      
      // React to show processed
      await ws.addReaction(msg.id, '✅')
    }
  }
  
  return createdTasks
}

Calendar

Find Meeting Conflicts

async function findConflicts(eventId: string) {
  const event = await ws.getCalendarEvent(eventId)
  
  // Get all events in same time range
  const rangeEvents = await ws.listCalendarEvents({
    start: event.startTime,
    end: event.endTime
  })
  
  // Find overlapping events with shared attendees
  const conflicts = rangeEvents.events.filter(e => {
    if (e.id === eventId) return false
    
    const sharedAttendees = e.attendees.filter(a => 
      event.attendees.some(ea => ea.userId === a.userId)
    )
    
    return sharedAttendees.length > 0
  })
  
  return conflicts
}

Schedule Follow-up Meeting

async function scheduleFollowUp(originalEventId: string) {
  const original = await ws.getCalendarEvent(originalEventId)
  
  // Schedule 1 week later
  const startTime = new Date(original.startTime)
  startTime.setDate(startTime.getDate() + 7)
  
  const endTime = new Date(original.endTime)
  endTime.setDate(endTime.getDate() + 7)
  
  const followUp = await ws.createCalendarEvent({
    title: `Follow-up: ${original.title}`,
    startTime: startTime.toISOString(),
    endTime: endTime.toISOString(),
    attendees: original.attendees.map(a => a.userId),
    description: `Follow-up to ${original.title}`
  })
  
  // Notify attendees
  await ws.sendChannelMessage('channel_general', {
    content: Message
      .text(`📅 Follow-up scheduled: `)
      .mention('file', followUp.event.title)
      .toInput()
  })
  
  return followUp
}

Integration Patterns

Webhook Handler

import { verifyAppRequestSignature, COLINE_SIGNATURE_HEADER, COLINE_TIMESTAMP_HEADER } from '@colineapp/sdk'

app.post('/webhooks/coline', async (req, res) => {
  // Verify signature
  const signature = req.headers[COLINE_SIGNATURE_HEADER] as string
  const timestamp = req.headers[COLINE_TIMESTAMP_HEADER] as string
  
  const isValid = await verifyAppRequestSignature({
    secret: process.env.COLINE_WEBHOOK_SECRET,
    signature,
    timestamp,
    body: JSON.stringify(req.body)
  })
  
  if (!isValid) {
    return res.status(401).send('Invalid signature')
  }
  
  const { type, data } = req.body
  
  switch (type) {
    case 'file.created':
      await handleFileCreated(data)
      break
    case 'task.updated':
      await handleTaskUpdated(data)
      break
    case 'message.created':
      await handleMessageCreated(data)
      break
  }
  
  res.send('OK')
})

async function handleFileCreated(data: { fileId: string }) {
  const file = await ws.getFile(data.fileId)
  
  // Process new file
  if (file.fileType === 'sheet') {
    // Import to external system
    await importToDatabase(file)
  }
}

OAuth Integration

import { createOauthState, createPkceCodeChallenge } from '@colineapp/sdk'

// Step 1: Generate authorization URL
app.get('/auth/coline', (req, res) => {
  const state = createOauthState()
  const { codeVerifier, codeChallenge } = createPkceCodeChallenge()
  
  // Store for later
  req.session.oauthState = state
  req.session.codeVerifier = codeVerifier
  
  const authUrl = client.buildLoginWithColineAuthorizeUrl({
    clientId: CLIENT_ID,
    redirectUri: 'https://your-app.com/auth/callback',
    scope: 'files:read messages:read',
    state,
    codeChallenge,
    codeChallengeMethod: 'S256'
  })
  
  res.redirect(authUrl)
})

// Step 2: Handle callback
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state')
  }
  
  // Exchange code for token
  const tokenResponse = await fetch('https://api.coline.app/v1/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://your-app.com/auth/callback',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code_verifier: req.session.codeVerifier
    })
  })
  
  const { access_token } = await tokenResponse.json()
  
  // Store token and redirect
  req.session.colineToken = access_token
  res.redirect('/dashboard')
})

Error Handling

Retry with Exponential Backoff

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (isColineApiError(error) && error.isRetryable) {
        const delay = Math.pow(2, i) * 1000
        console.log(`Retrying in ${delay}ms...`)
        await sleep(delay)
        continue
      }
      throw error
    }
  }
  throw new Error('Max retries exceeded')
}

// Usage
const file = await withRetry(() => ws.getFile('file_123'))

Graceful Degradation

async function getFileWithFallback(fileId: string) {
  try {
    return await ws.getFile(fileId)
  } catch (error) {
    if (isColineApiError(error)) {
      if (error.status === 404) {
        console.log('File not found, using default')
        return getDefaultFile()
      }
      if (error.status === 403) {
        console.log('No access, returning restricted view')
        return getRestrictedView(fileId)
      }
    }
    throw error
  }
}

Testing

Mock Client for Tests

// test-helpers.ts
export function createMockClient() {
  return {
    workspace: () => ({
      getFile: jest.fn(),
      listDriveFiles: jest.fn(),
      sendChannelMessage: jest.fn()
    })
  }
}

// my-feature.test.ts
const mockWs = createMockClient()

mockWs.workspace().getFile.mockResolvedValue({
  id: 'file_123',
  name: 'test.pdf'
})

// Test your code
const result = await processFile(mockWs, 'file_123')
expect(result).toBeDefined()

Next Steps

On this page