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' })
)
Related Documentation
- API Reference - Complete API documentation
- Webhooks - Event notifications
- SDK - Client libraries
- Security - Security best practices
Need customization help? Contact our enterprise support team.