The WorkOS Audit Logs API enables you to create tamper-proof, searchable audit trails for compliance, security investigations, and customer transparency.
Overview
Audit logs capture:
Who performed an action (actor)
What action was performed
When it happened
Where it happened (location, IP)
What was affected (targets)
WorkOS stores audit logs securely and makes them available through the Admin Portal, API exports, and Log Streams.
Core Concepts
Audit Log Events
An audit log event represents a single action:
interface AuditLogEvent {
action : string ; // e.g., 'user.created'
version ?: number ; // Schema version
occurredAt : Date ; // When it happened
actor : AuditLogActor ; // Who did it
targets : AuditLogTarget []; // What was affected
context : { // Where it happened
location : string ; // IP address
userAgent ?: string ;
};
metadata ?: Record < string , any >; // Additional data
}
Actors
Actors represent who performed the action:
interface AuditLogActor {
id : string ; // Unique identifier (user_123)
name ?: string ; // Display name (John Doe)
type : string ; // Actor type (user, system, api_key)
metadata ?: Record < string , string | number | boolean >;
}
Targets
Targets represent what was affected:
interface AuditLogTarget {
id : string ; // Unique identifier
name ?: string ; // Display name
type : string ; // Resource type (document, user, organization)
metadata ?: Record < string , string | number | boolean >;
}
Creating Audit Logs
Basic Event
import { WorkOS } from '@workos-inc/node' ;
const workos = new WorkOS ( 'sk_...' );
await workos . auditLogs . createEvent (
'org_123' , // Organization ID
{
action: 'user.login' ,
occurredAt: new Date (),
actor: {
id: 'user_456' ,
name: 'John Doe' ,
type: 'user' ,
},
targets: [
{
id: 'user_456' ,
type: 'user' ,
},
],
context: {
location: '203.0.113.1' ,
userAgent: 'Mozilla/5.0 ...' ,
},
}
);
Express Middleware
Automatically log all authenticated requests:
import { Request , Response , NextFunction } from 'express' ;
import { WorkOS } from '@workos-inc/node' ;
const workos = new WorkOS ( 'sk_...' );
function auditLogger ( action : string ) {
return async ( req : Request , res : Response , next : NextFunction ) => {
// Capture initial request data
const startTime = Date . now ();
// Wait for response to complete
res . on ( 'finish' , async () => {
try {
const organizationId = req . organizationId ;
if ( ! organizationId ) return ;
await workos . auditLogs . createEvent ( organizationId , {
action ,
occurredAt: new Date ( startTime ),
actor: {
id: req . user . id ,
name: req . user . name ,
type: 'user' ,
metadata: {
email: req . user . email ,
},
},
targets: extractTargets ( req ),
context: {
location: req . ip || req . socket . remoteAddress || 'unknown' ,
userAgent: req . get ( 'user-agent' ),
},
metadata: {
method: req . method ,
path: req . path ,
statusCode: res . statusCode ,
duration: Date . now () - startTime ,
},
});
} catch ( error ) {
// Never block requests due to audit log failures
console . error ( 'Failed to create audit log:' , error );
}
});
next ();
};
}
function extractTargets ( req : Request ) : AuditLogTarget [] {
const targets : AuditLogTarget [] = [];
// Extract from route parameters
if ( req . params . userId ) {
targets . push ({
id: req . params . userId ,
type: 'user' ,
});
}
if ( req . params . documentId ) {
targets . push ({
id: req . params . documentId ,
type: 'document' ,
});
}
return targets . length > 0 ? targets : [{ id: req . user . id , type: 'user' }];
}
// Usage
app . delete (
'/api/users/:userId' ,
requireAuth (),
auditLogger ( 'user.deleted' ),
async ( req , res ) => {
await deleteUser ( req . params . userId );
res . json ({ success: true });
}
);
Common Patterns
User Actions
Data Access
Administrative Actions
System Events
// User registration
await workos . auditLogs . createEvent ( organizationId , {
action: 'user.created' ,
occurredAt: new Date (),
actor: {
id: 'system' ,
type: 'system' ,
},
targets: [{
id: newUser . id ,
name: newUser . email ,
type: 'user' ,
}],
context: { location: req . ip },
});
// Password change
await workos . auditLogs . createEvent ( organizationId , {
action: 'user.password_changed' ,
occurredAt: new Date (),
actor: {
id: user . id ,
name: user . name ,
type: 'user' ,
},
targets: [{ id: user . id , type: 'user' }],
context: {
location: req . ip ,
userAgent: req . get ( 'user-agent' ),
},
});
Idempotency
Audit log events are automatically idempotent:
// SDK auto-generates idempotency key
await workos . auditLogs . createEvent ( organizationId , event );
For custom idempotency:
await workos . auditLogs . createEvent (
organizationId ,
event ,
{
idempotencyKey: `user-login- ${ userId } - ${ timestamp } ` ,
}
);
Idempotency keys are valid for 24 hours. The SDK generates UUIDs by default using crypto.randomUUID().
Exporting Audit Logs
Export audit logs for compliance, archival, or analysis:
// Create export
const exportResult = await workos . auditLogs . createExport ({
organizationId: 'org_123' ,
rangeStart: new Date ( '2024-01-01' ),
rangeEnd: new Date ( '2024-01-31' ),
actions: [ 'user.created' , 'user.deleted' , 'data.exported' ],
actorNames: [ 'John Doe' , 'Jane Smith' ],
actorIds: [ 'user_456' , 'user_789' ],
});
console . log ( exportResult . id ); // audit_log_export_123
console . log ( exportResult . state ); // pending
Polling for Completion
async function waitForExport ( exportId : string ) : Promise < string > {
let attempts = 0 ;
const maxAttempts = 30 ;
while ( attempts < maxAttempts ) {
const exportData = await workos . auditLogs . getExport ( exportId );
if ( exportData . state === 'ready' ) {
return exportData . url ! ;
}
if ( exportData . state === 'error' ) {
throw new Error ( 'Export failed' );
}
// Wait 2 seconds before next check
await new Promise ( resolve => setTimeout ( resolve , 2000 ));
attempts ++ ;
}
throw new Error ( 'Export timeout' );
}
const downloadUrl = await waitForExport ( exportResult . id );
console . log ( 'Download:' , downloadUrl );
Automated Monthly Exports
import cron from 'node-cron' ;
// Run on first day of each month at 1 AM
cron . schedule ( '0 1 1 * *' , async () => {
const now = new Date ();
const lastMonth = new Date ( now . getFullYear (), now . getMonth () - 1 , 1 );
const lastMonthEnd = new Date ( now . getFullYear (), now . getMonth (), 0 );
const organizations = await getOrganizations ();
for ( const org of organizations ) {
try {
const exportResult = await workos . auditLogs . createExport ({
organizationId: org . id ,
rangeStart: lastMonth ,
rangeEnd: lastMonthEnd ,
});
const url = await waitForExport ( exportResult . id );
// Archive to S3, send to SIEM, etc.
await archiveAuditLogs ( org . id , url );
} catch ( error ) {
console . error ( `Failed to export for ${ org . id } :` , error );
}
}
});
Audit Log Schemas
Define structured schemas for consistent audit log formats:
await workos . auditLogs . createSchema (
{
action: 'user.created' ,
targets: [
{
type: 'user' ,
fields: [
{
name: 'email' ,
type: 'string' ,
required: true ,
},
{
name: 'role' ,
type: 'string' ,
required: false ,
},
],
},
],
},
{
idempotencyKey: 'schema-user-created' ,
}
);
Schemas help ensure:
Consistent audit log structure
Required fields are present
Proper data types
Better searchability in Admin Portal
Advanced Patterns
Audit Log Decorator
function Audit ( action : string ) {
return function (
target : any ,
propertyKey : string ,
descriptor : PropertyDescriptor
) {
const originalMethod = descriptor . value ;
descriptor . value = async function ( ... args : any []) {
const startTime = Date . now ();
const context = this ; // Class instance
try {
const result = await originalMethod . apply ( context , args );
// Log success
await workos . auditLogs . createEvent ( context . organizationId , {
action: ` ${ action } .success` ,
occurredAt: new Date ( startTime ),
actor: {
id: context . userId ,
type: 'user' ,
},
targets: extractTargetsFromArgs ( args ),
context: {
location: context . ipAddress ,
},
metadata: {
duration: Date . now () - startTime ,
},
});
return result ;
} catch ( error ) {
// Log failure
await workos . auditLogs . createEvent ( context . organizationId , {
action: ` ${ action } .failed` ,
occurredAt: new Date ( startTime ),
actor: {
id: context . userId ,
type: 'user' ,
},
targets: extractTargetsFromArgs ( args ),
context: {
location: context . ipAddress ,
},
metadata: {
error: error . message ,
duration: Date . now () - startTime ,
},
});
throw error ;
}
};
return descriptor ;
};
}
class UserService {
@ Audit ( 'user.deleted' )
async deleteUser ( userId : string ) {
// Delete user logic
}
}
Batch Logging with Queue
import Queue from 'bull' ;
const auditQueue = new Queue ( 'audit-logs' , 'redis://localhost:6379' );
auditQueue . process ( async ( job ) => {
const { organizationId , event } = job . data ;
await workos . auditLogs . createEvent ( organizationId , event );
});
// Add to queue instead of blocking requests
function queueAuditLog ( organizationId : string , event : AuditLogEvent ) {
auditQueue . add ({ organizationId , event }, {
attempts: 3 ,
backoff: {
type: 'exponential' ,
delay: 2000 ,
},
});
}
app . post ( '/api/users' , async ( req , res ) => {
const user = await createUser ( req . body );
// Queue audit log asynchronously
queueAuditLog ( req . organizationId , {
action: 'user.created' ,
occurredAt: new Date (),
actor: { id: req . user . id , type: 'user' },
targets: [{ id: user . id , type: 'user' }],
context: { location: req . ip },
});
res . json ( user );
});
Best Practices
Never Block User Requests
Wrap audit log calls in try-catch and handle failures gracefully: try {
await workos . auditLogs . createEvent ( organizationId , event );
} catch ( error ) {
// Log error but don't fail the request
console . error ( 'Audit log failed:' , error );
}
Use Meaningful Action Names
Follow a consistent naming convention:
resource.action (e.g., user.created, document.deleted)
Be specific: settings.mfa_enabled not settings.updated
Include Rich Context
Capture IP addresses, user agents, and relevant metadata for investigations: context : {
location : req . ip || 'unknown' ,
userAgent : req . get ( 'user-agent' ),
},
metadata : {
method : req . method ,
path : req . path ,
statusCode : res . statusCode ,
}
Log Both Success and Failure
Track failed attempts for security monitoring: action : success ? 'user.login' : 'user.login_failed' ,
metadata : success ? {} : { reason: 'invalid_password' },
Use Idempotency for Retries
Prevent duplicate logs when retrying failed requests: idempotencyKey : ` ${ action } - ${ resourceId } - ${ timestamp } `
Compliance Considerations
Audit logs help meet requirements for:
SOC 2 : System monitoring and logging
HIPAA : Access logs for protected health information
GDPR : Records of processing activities
PCI DSS : Tracking access to cardholder data
For retention policies, WorkOS stores audit logs for:
Standard : 1 year
Enterprise : Custom retention periods available
API Reference
See the source code for implementation details:
createEvent() - src/audit-logs/audit-logs.ts:28
createExport() - src/audit-logs/audit-logs.ts:51
getExport() - src/audit-logs/audit-logs.ts:60
createSchema() - src/audit-logs/audit-logs.ts:68