Verk

Advanced Customization

Custom integrations, advanced workflows, enterprise features

Advanced Customization

Build custom integrations, create advanced automation workflows, and leverage enterprise features to tailor Verk to your organization's unique needs.

Overview

Verk's customization capabilities allow you to:

  • Build custom integrations with any external system
  • Create complex automation workflows
  • Implement custom authentication (SSO)
  • Design custom UI themes and branding
  • Develop custom field types and validations
  • Build embedded Verk experiences

Custom Integrations

Building Custom Integrations

Create seamless connections between Verk and your internal systems.

Architecture Overview:

// integration-server.ts
import { VerkClient } from '@verk/sdk'
import express from 'express'

const app = express()
const verk = new VerkClient({ apiKey: process.env.VERK_API_KEY })

// Webhook endpoint to receive Verk events
app.post('/verk-webhook', async (req, res) => {
  const { event, data } = req.body

  // Process event and sync to your system
  await syncToInternalSystem(event, data)

  res.status(200).send('OK')
})

// API endpoint for your system to create Verk tasks
app.post('/create-task', async (req, res) => {
  const { title, description, priority } = req.body

  const task = await verk.tasks.create({
    title,
    description,
    priority,
    projectId: process.env.DEFAULT_PROJECT_ID,
  })

  res.json(task)
})

app.listen(3000)

Bi-directional Sync

Example: Sync with CRM System

// crm-sync.ts
import { VerkClient } from '@verk/sdk'
import { CRMClient } from './crm-client'

class VerkCRMSync {
  constructor(
    private verk: VerkClient,
    private crm: CRMClient
  ) {}

  // Sync Verk task to CRM
  async syncTaskToCRM(taskId: string) {
    const task = await this.verk.tasks.get(taskId)

    // Check if CRM record exists
    const crmRecord = await this.crm.findByExternalId(taskId)

    if (crmRecord) {
      // Update existing record
      await this.crm.updateDeal(crmRecord.id, {
        title: task.title,
        status: this.mapStatus(task.status),
        value: task.customFields?.estimatedValue,
      })
    } else {
      // Create new record
      await this.crm.createDeal({
        title: task.title,
        status: this.mapStatus(task.status),
        externalId: taskId,
      })
    }
  }

  // Sync CRM deal to Verk
  async syncCRMToTask(dealId: string) {
    const deal = await this.crm.getDeal(dealId)

    // Find or create Verk task
    const tasks = await this.verk.tasks.list({
      customFields: { crmDealId: dealId },
    })

    if (tasks.length > 0) {
      // Update existing task
      await this.verk.tasks.update(tasks[0].id, {
        title: deal.title,
        status: this.mapCRMStatus(deal.status),
      })
    } else {
      // Create new task
      await this.verk.tasks.create({
        title: deal.title,
        projectId: this.getProjectForDeal(deal),
        customFields: { crmDealId: dealId },
      })
    }
  }

  private mapStatus(verkStatus: string): string {
    const statusMap = {
      todo: 'open',
      in_progress: 'in_progress',
      done: 'won',
    }
    return statusMap[verkStatus] || 'open'
  }
}

// Usage
const sync = new VerkCRMSync(verk, crm)

// Set up webhooks from both systems
verkWebhook.on('task.updated', async task => {
  await sync.syncTaskToCRM(task.id)
})

crmWebhook.on('deal.updated', async deal => {
  await sync.syncCRMToTask(deal.id)
})

Custom Integration Framework

// integration-framework.ts
interface Integration {
  name: string
  authenticate(): Promise<void>
  sync(direction: 'to_verk' | 'from_verk', data: any): Promise<void>
  validateConfig(config: any): boolean
}

abstract class BaseIntegration implements Integration {
  abstract name: string

  constructor(
    protected verk: VerkClient,
    protected config: any
  ) {}

  abstract authenticate(): Promise<void>
  abstract sync(direction: 'to_verk' | 'from_verk', data: any): Promise<void>
  abstract validateConfig(config: any): boolean

  // Shared helper methods
  protected async mapAndCreateTask(externalData: any) {
    const mapped = this.mapToVerkTask(externalData)
    return await this.verk.tasks.create(mapped)
  }

  protected abstract mapToVerkTask(data: any): CreateTaskRequest
}

// Example: Jira Integration
class JiraIntegration extends BaseIntegration {
  name = 'jira'
  private jiraClient: JiraClient

  async authenticate() {
    this.jiraClient = new JiraClient({
      host: this.config.jiraHost,
      username: this.config.username,
      password: this.config.apiToken,
    })
  }

  async sync(direction: 'to_verk' | 'from_verk', data: any) {
    if (direction === 'to_verk') {
      await this.syncJiraToVerk(data)
    } else {
      await this.syncVerkToJira(data)
    }
  }

  private async syncJiraToVerk(issue: any) {
    const task = this.mapToVerkTask(issue)
    await this.verk.tasks.create(task)
  }

  protected mapToVerkTask(issue: any) {
    return {
      title: issue.fields.summary,
      description: issue.fields.description,
      status: this.mapJiraStatus(issue.fields.status.name),
      priority: this.mapJiraPriority(issue.fields.priority.name),
      customFields: {
        jiraKey: issue.key,
        jiraId: issue.id,
      },
    }
  }

  validateConfig(config: any): boolean {
    return !!(config.jiraHost && config.username && config.apiToken)
  }

  private mapJiraStatus(status: string): string {
    const map = {
      'To Do': 'todo',
      'In Progress': 'in_progress',
      Done: 'done',
    }
    return map[status] || 'todo'
  }
}

Advanced Workflows

Complex Automation Rules

Multi-step Workflow with Conditions

// workflow-engine.ts
interface WorkflowStep {
  id: string
  type: 'action' | 'condition' | 'delay'
  execute(context: any): Promise<any>
}

class WorkflowEngine {
  private steps: WorkflowStep[] = []

  addStep(step: WorkflowStep) {
    this.steps.push(step)
    return this
  }

  async execute(initialContext: any) {
    let context = { ...initialContext }

    for (const step of this.steps) {
      try {
        const result = await step.execute(context)
        context = { ...context, ...result }
      } catch (error) {
        console.error(`Step ${step.id} failed:`, error)
        throw error
      }
    }

    return context
  }
}

// Example workflow: Auto-assign tasks based on workload
class AutoAssignWorkflow {
  constructor(private verk: VerkClient) {}

  async run(taskId: string) {
    const workflow = new WorkflowEngine()

    // Step 1: Get task details
    workflow.addStep({
      id: 'get-task',
      type: 'action',
      execute: async () => {
        const task = await this.verk.tasks.get(taskId)
        return { task }
      },
    })

    // Step 2: Check if high priority
    workflow.addStep({
      id: 'check-priority',
      type: 'condition',
      execute: async context => {
        if (context.task.priority !== 'high') {
          throw new Error('Not high priority, skip workflow')
        }
        return {}
      },
    })

    // Step 3: Get team members
    workflow.addStep({
      id: 'get-members',
      type: 'action',
      execute: async () => {
        const members = await this.verk.members.list()
        return { members }
      },
    })

    // Step 4: Calculate workload
    workflow.addStep({
      id: 'calculate-workload',
      type: 'action',
      execute: async context => {
        const workloads = await Promise.all(
          context.members.map(async member => {
            const tasks = await this.verk.tasks.list({
              assigneeId: member.id,
              status: 'in_progress',
            })
            return { memberId: member.id, taskCount: tasks.length }
          })
        )

        // Find member with lowest workload
        const assignee = workloads.sort((a, b) => a.taskCount - b.taskCount)[0]
        return { assigneeId: assignee.memberId }
      },
    })

    // Step 5: Assign task
    workflow.addStep({
      id: 'assign-task',
      type: 'action',
      execute: async context => {
        await this.verk.tasks.update(taskId, {
          assigneeId: context.assigneeId,
          status: 'in_progress',
        })
        return {}
      },
    })

    await workflow.execute({ taskId })
  }
}

Scheduled Workflows

// scheduled-workflows.ts
import cron from 'node-cron'

class ScheduledWorkflows {
  constructor(private verk: VerkClient) {}

  // Daily digest email
  setupDailyDigest() {
    cron.schedule('0 9 * * *', async () => {
      const members = await this.verk.members.list()

      for (const member of members) {
        const tasks = await this.verk.tasks.list({
          assigneeId: member.id,
          dueDate: { lte: new Date() },
        })

        if (tasks.length > 0) {
          await this.sendDigestEmail(member, tasks)
        }
      }
    })
  }

  // Weekly report generation
  setupWeeklyReport() {
    cron.schedule('0 10 * * MON', async () => {
      const startDate = new Date()
      startDate.setDate(startDate.getDate() - 7)

      const completedTasks = await this.verk.tasks.list({
        status: 'done',
        updatedAt: { gte: startDate },
      })

      await this.generateWeeklyReport(completedTasks)
    })
  }

  // Overdue task escalation
  setupOverdueEscalation() {
    cron.schedule('0 */4 * * *', async () => {
      const overdueTasks = await this.verk.tasks.list({
        dueDate: { lt: new Date() },
        status: ['todo', 'in_progress'],
      })

      for (const task of overdueTasks) {
        await this.escalateTask(task)
      }
    })
  }

  private async escalateTask(task: any) {
    // Notify manager
    const manager = await this.getManager(task.assigneeId)

    await this.verk.comments.create(task.id, {
      content: `@${manager.id} This task is overdue and needs attention.`,
      mentions: [manager.id],
    })

    // Increase priority
    if (task.priority !== 'high') {
      await this.verk.tasks.update(task.id, {
        priority: 'high',
      })
    }
  }
}

Enterprise Features

Single Sign-On (SSO)

SAML 2.0 Integration

// saml-config.ts
import { Strategy as SamlStrategy } from 'passport-saml'

export const samlConfig = {
  entryPoint: process.env.SAML_ENTRY_POINT,
  issuer: process.env.SAML_ISSUER,
  callbackUrl: `${process.env.APP_URL}/auth/saml/callback`,
  cert: process.env.SAML_CERT,

  // Attribute mapping
  identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',

  // User profile mapping
  attributeMap: {
    email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
    firstName:
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
    lastName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
  },
}

// Passport SAML strategy
passport.use(
  new SamlStrategy(samlConfig, async (profile, done) => {
    try {
      // Find or create user in Verk
      let user = await findUserByEmail(profile.email)

      if (!user) {
        user = await createUser({
          email: profile.email,
          firstName: profile.firstName,
          lastName: profile.lastName,
          ssoProvider: 'saml',
        })
      }

      return done(null, user)
    } catch (error) {
      return done(error)
    }
  })
)

OAuth 2.0 / OIDC Integration

// oauth-config.ts
import { Strategy as OAuthStrategy } from 'passport-oauth2'

export const oauthConfig = {
  authorizationURL: process.env.OAUTH_AUTH_URL,
  tokenURL: process.env.OAUTH_TOKEN_URL,
  clientID: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET,
  callbackURL: `${process.env.APP_URL}/auth/oauth/callback`,
  scope: ['openid', 'profile', 'email'],
}

passport.use(
  new OAuthStrategy(
    oauthConfig,
    async (accessToken, refreshToken, profile, done) => {
      // Verify and create/update user
      const user = await syncUserFromOAuth(profile)
      return done(null, user)
    }
  )
)

Custom Field Types

Define Custom Field Type

// custom-field-types.ts
interface CustomFieldType {
 id: string;
 name: string;
 validate(value: any): boolean;
 format(value: any): string;
 render(value: any): React.ReactNode;
}

class CurrencyFieldType implements CustomFieldType {
 id = 'currency';
 name = 'Currency';

 validate(value: any): boolean {
 return typeof value === 'number' && value >= 0;
 }

 format(value: any): string {
 return new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
 }).format(value);
 }

 render(value: any): React.ReactNode {
 return <span className="currency">{this.format(value)}</span>;
 }
}

class RatingFieldType implements CustomFieldType {
 id = 'rating';
 name = 'Rating';

 validate(value: any): boolean {
 return typeof value === 'number' && value >= 1 && value <= 5;
 }

 format(value: any): string {
 return ''.repeat(value);
 }

 render(value: any): React.ReactNode {
 return (
  <div className="rating">
  {Array.from({ length: 5 }).map((_, i) => (
   <span key={i} className={i < value ? 'filled' : 'empty'}>

   </span>
  ))}
  </div>
 );
 }
}

// Register custom field types
const fieldTypeRegistry = new Map<string, CustomFieldType>();
fieldTypeRegistry.set('currency', new CurrencyFieldType());
fieldTypeRegistry.set('rating', new RatingFieldType());

Custom Validation Rules

// validation-rules.ts
interface ValidationRule {
  name: string
  validate(value: any, context: any): Promise<boolean>
  message: string
}

class TaskValidationEngine {
  private rules: ValidationRule[] = []

  addRule(rule: ValidationRule) {
    this.rules.push(rule)
    return this
  }

  async validate(task: any, context: any) {
    const errors: string[] = []

    for (const rule of this.rules) {
      try {
        const isValid = await rule.validate(task, context)
        if (!isValid) {
          errors.push(rule.message)
        }
      } catch (error) {
        errors.push(`Validation error: ${error.message}`)
      }
    }

    return {
      isValid: errors.length === 0,
      errors,
    }
  }
}

// Example custom validation rules
const requiresApprovalRule: ValidationRule = {
  name: 'requires-approval',
  validate: async (task, context) => {
    if (task.customFields?.budget > 10000) {
      return !!task.customFields?.approvedBy
    }
    return true
  },
  message: 'Tasks with budget over $10,000 require approval',
}

const assignmentRule: ValidationRule = {
  name: 'assignment-required',
  validate: async (task, context) => {
    if (task.priority === 'high') {
      return !!task.assigneeId
    }
    return true
  },
  message: 'High priority tasks must be assigned',
}

// Usage
const validator = new TaskValidationEngine()
validator.addRule(requiresApprovalRule)
validator.addRule(assignmentRule)

const result = await validator.validate(task, { user: currentUser })
if (!result.isValid) {
  console.error('Validation errors:', result.errors)
}

Embedded Verk

Iframe Embedding

Basic Embedding

<!-- embed-verk.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Verk Embedded</title>
    <style>
      #verk-container {
        width: 100%;
        height: 800px;
        border: none;
      }
    </style>
  </head>
  <body>
    <iframe
      id="verk-container"
      src="https://app.verk.com/embed?project=proj_123&api_key=YOUR_API_KEY"
      allow="clipboard-write"
      sandbox="allow-scripts allow-same-origin allow-forms"
    ></iframe>

    <script>
      // Handle messages from iframe
      window.addEventListener('message', event => {
        if (event.origin !== 'https://app.verk.com') return

        console.log('Message from Verk:', event.data)

        if (event.data.type === 'task.created') {
          console.log('New task created:', event.data.task)
        }
      })

      // Send message to iframe
      const iframe = document.getElementById('verk-container')
      iframe.contentWindow.postMessage(
        {
          type: 'navigate',
          path: '/tasks/123',
        },
        'https://app.verk.com'
      )
    </script>
  </body>
</html>

React Component Integration

// VerkEmbed.tsx
import React, { useEffect, useRef } from 'react';

interface VerkEmbedProps {
 projectId: string;
 apiKey: string;
 onTaskCreated?: (task: any) => void;
 onTaskUpdated?: (task: any) => void;
}

export const VerkEmbed: React.FC<VerkEmbedProps> = ({
 projectId,
 apiKey,
 onTaskCreated,
 onTaskUpdated
}) => {
 const iframeRef = useRef<HTMLIFrameElement>(null);

 useEffect(() => {
 const handleMessage = (event: MessageEvent) => {
  if (event.origin !== 'https://app.verk.com') return;

  const { type, data } = event.data;

  switch (type) {
  case 'task.created':
   onTaskCreated?.(data);
   break;
  case 'task.updated':
   onTaskUpdated?.(data);
   break;
  }
 };

 window.addEventListener('message', handleMessage);
 return () => window.removeEventListener('message', handleMessage);
 }, [onTaskCreated, onTaskUpdated]);

 const sendMessage = (message: any) => {
 iframeRef.current?.contentWindow?.postMessage(
  message,
  'https://app.verk.com'
 );
 };

 return (
 <iframe
  ref={iframeRef}
  src={`https://app.verk.com/embed?project=${projectId}&api_key=${apiKey}`}
  style=`{{ width: '100%', height: '100%', border: 'none' }}`
  allow="clipboard-write"
  sandbox="allow-scripts allow-same-origin allow-forms"
 />
 );
};

// Usage
function App() {
 return (
 <VerkEmbed
  projectId="proj_123"
  apiKey={process.env.VERK_API_KEY}
  onTaskCreated={(task) => {
  console.log('Task created:', task);
  // Sync to your database
  syncTaskToDatabase(task);
  }}`
  onTaskUpdated={(task) => {
  console.log('Task updated:', task);
  }}`
 />
 );
}

Custom UI Themes

Theme Configuration

// theme-config.ts
interface VerkTheme {
  colors: {
    primary: string
    secondary: string
    success: string
    danger: string
    warning: string
    background: string
    surface: string
    text: string
  }
  typography: {
    fontFamily: string
    fontSize: {
      sm: string
      md: string
      lg: string
      xl: string
    }
  }
  spacing: {
    unit: number
  }
  borderRadius: string
}

const customTheme: VerkTheme = {
  colors: {
    primary: '#6366f1',
    secondary: '#8b5cf6',
    success: '#10b981',
    danger: '#ef4444',
    warning: '#f59e0b',
    background: '#f9fafb',
    surface: '#ffffff',
    text: '#111827',
  },
  typography: {
    fontFamily: 'Inter, system-ui, sans-serif',
    fontSize: {
      sm: '0.875rem',
      md: '1rem',
      lg: '1.125rem',
      xl: '1.25rem',
    },
  },
  spacing: {
    unit: 8,
  },
  borderRadius: '8px',
}

// Apply theme via API
await verk.organizations.updateSettings({
  theme: customTheme,
})

Custom CSS

/* custom-verk-theme.css */
:root {
  --verk-primary: #6366f1;
  --verk-secondary: #8b5cf6;
  --verk-success: #10b981;
  --verk-danger: #ef4444;
  --verk-warning: #f59e0b;
  --verk-bg: #f9fafb;
  --verk-surface: #ffffff;
  --verk-text: #111827;
  --verk-border-radius: 8px;
  --verk-font-family: 'Inter', system-ui, sans-serif;
}

/* Customize task cards */
.verk-task-card {
  border-left: 4px solid var(--verk-primary);
  border-radius: var(--verk-border-radius);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Customize buttons */
.verk-button-primary {
  background-color: var(--verk-primary);
  border-radius: var(--verk-border-radius);
  font-family: var(--verk-font-family);
}

/* Customize sidebar */
.verk-sidebar {
  background-color: var(--verk-surface);
  border-right: 1px solid #e5e7eb;
}

/* Dark mode support */
@media (prefers-color-scheme: dark) {
  :root {
    --verk-bg: #111827;
    --verk-surface: #1f2937;
    --verk-text: #f9fafb;
  }
}

Performance Optimization

Caching Strategy

// cache-manager.ts
import Redis from 'ioredis'

class VerkCacheManager {
  private redis: Redis
  private verk: VerkClient

  constructor(redisUrl: string, verkApiKey: string) {
    this.redis = new Redis(redisUrl)
    this.verk = new VerkClient({ apiKey: verkApiKey })
  }

  async getTask(taskId: string) {
    // Try cache first
    const cached = await this.redis.get(`task:${taskId}`)
    if (cached) {
      return JSON.parse(cached)
    }

    // Fetch from API
    const task = await this.verk.tasks.get(taskId)

    // Cache for 5 minutes
    await this.redis.setex(`task:${taskId}`, 300, JSON.stringify(task))

    return task
  }

  async invalidateTask(taskId: string) {
    await this.redis.del(`task:${taskId}`)
  }

  async getProjectTasks(projectId: string) {
    const cacheKey = `project:${projectId}:tasks`
    const cached = await this.redis.get(cacheKey)

    if (cached) {
      return JSON.parse(cached)
    }

    const tasks = await this.verk.tasks.list({ projectId })
    await this.redis.setex(cacheKey, 60, JSON.stringify(tasks))

    return tasks
  }
}

Batch API Requests

// batch-processor.ts
class BatchProcessor {
  private queue: any[] = []
  private processing = false

  constructor(
    private verk: VerkClient,
    private batchSize = 10,
    private delayMs = 100
  ) {}

  async addTask(task: any) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject })

      if (!this.processing) {
        this.processBatch()
      }
    })
  }

  private async processBatch() {
    this.processing = true

    while (this.queue.length > 0) {
      const batch = this.queue.splice(0, this.batchSize)

      try {
        const results = await Promise.all(
          batch.map(({ task }) => this.verk.tasks.create(task))
        )

        batch.forEach(({ resolve }, index) => {
          resolve(results[index])
        })
      } catch (error) {
        batch.forEach(({ reject }) => {
          reject(error)
        })
      }

      if (this.queue.length > 0) {
        await new Promise(resolve => setTimeout(resolve, this.delayMs))
      }
    }

    this.processing = false
  }
}

// Usage
const batchProcessor = new BatchProcessor(verk)

// Add multiple tasks - they'll be batched automatically
const tasks = await Promise.all([
  batchProcessor.addTask({ title: 'Task 1', projectId: 'proj_123' }),
  batchProcessor.addTask({ title: 'Task 2', projectId: 'proj_123' }),
  batchProcessor.addTask({ title: 'Task 3', projectId: 'proj_123' }),
])

Security Considerations

API Key Rotation

// api-key-rotation.ts
class APIKeyRotationManager {
  private verk: VerkClient
  private currentKey: string
  private newKey: string | null = null

  async rotateKey() {
    // Generate new API key
    this.newKey = await this.verk.apiKeys.create({
      name: 'Rotated Key',
      scopes: ['tasks:read', 'tasks:write'],
    })

    // Grace period: Both keys work for 24 hours
    await this.sleep(24 * 60 * 60 * 1000)

    // Delete old key
    await this.verk.apiKeys.delete(this.currentKey)

    this.currentKey = this.newKey
    this.newKey = null
  }

  private sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

Request Signing

// request-signing.ts
import crypto from 'crypto'

function signRequest(
  method: string,
  path: string,
  body: string,
  secret: string
): string {
  const timestamp = Date.now()
  const payload = `${method}:${path}:${timestamp}:${body}`

  const signature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return `${timestamp}.${signature}`
}

// Usage in API client
const signature = signRequest(
  'POST',
  '/api/tasks',
  JSON.stringify(taskData),
  apiSecret
)

await fetch('https://api.verk.com/v1/tasks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'X-Verk-Signature': signature,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(taskData),
})

Best Practices

Rate Limit Handling

// rate-limit-handler.ts
class RateLimitHandler {
  private requestQueue: Array<() => Promise<any>> = []
  private isProcessing = false

  async execute<T>(request: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.requestQueue.push(async () => {
        try {
          const result = await request()
          resolve(result)
        } catch (error) {
          if (error.status === 429) {
            // Rate limited - retry with backoff
            const retryAfter = parseInt(error.headers['retry-after'] || '60')
            await this.sleep(retryAfter * 1000)
            return this.execute(request)
          }
          reject(error)
        }
      })

      if (!this.isProcessing) {
        this.processQueue()
      }
    })
  }

  private async processQueue() {
    this.isProcessing = true

    while (this.requestQueue.length > 0) {
      const request = this.requestQueue.shift()
      if (request) {
        await request()
        await this.sleep(100) // Small delay between requests
      }
    }

    this.isProcessing = false
  }

  private sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

Error Recovery

// error-recovery.ts
async function executeWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  backoffMs = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      if (attempt === maxRetries) {
        throw error
      }

      // Exponential backoff
      const delay = backoffMs * Math.pow(2, attempt - 1)
      await new Promise(resolve => setTimeout(resolve, delay))

      console.log(`Retry attempt ${attempt}/${maxRetries} after ${delay}ms`)
    }
  }

  throw new Error('Max retries exceeded')
}

// Usage
const task = await executeWithRetry(() =>
  verk.tasks.create({ title: 'New task', projectId: 'proj_123' })
)

Need customization help? Contact our enterprise support team.