Skip to main content

Caching Strategy

Ever Works uses a multi-layer caching approach built on NestJS CacheModule with cache-manager. Caching is used for pipeline checkpoints, plugin-scoped data, settings resolution, and remote cache proxying in distributed worker environments.

Key sources:

  • packages/agent/src/pipeline/step-pipeline-executor.service.ts -- Pipeline checkpoint caching
  • packages/agent/src/plugins/services/plugin-context-factory.service.ts -- Plugin-scoped caching
  • packages/plugin/src/contracts/plugin-context.interface.ts -- PluginCache interface
  • packages/tasks/src/trigger/worker/modules/trigger-remote-cache.module.ts -- Remote cache proxy

Architecture

Plugin-Scoped Cache

Each plugin receives its own cache instance through PluginContext. The cache is namespace-isolated using a plugin:<pluginId>: key prefix.

PluginCache Interface

interface PluginCache {
get<T>(key: string): Promise<T | undefined>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
has(key: string): Promise<boolean>;
clear(): Promise<void>;
}

Key Namespacing

The PluginContextFactoryService creates cache instances with automatic namespacing:

private createCache(pluginId: string): PluginCache {
const keyPrefix = `plugin:${pluginId}:`;

return {
get: async <T>(key: string): Promise<T | undefined> => {
return this.cacheManager.get<T>(`${keyPrefix}${key}`);
},
set: async <T>(key: string, value: T, ttl?: number): Promise<void> => {
await this.cacheManager.set(`${keyPrefix}${key}`, value, ttl);
},
delete: async (key: string): Promise<boolean> => {
await this.cacheManager.del(`${keyPrefix}${key}`);
return true;
},
has: async (key: string): Promise<boolean> => {
const value = await this.cacheManager.get(`${keyPrefix}${key}`);
return value !== undefined;
},
clear: async (): Promise<void> => {
// Note: prefix-based clearing not supported by cache-manager
// Would need custom implementation
},
};
}

Cache Key Examples

PluginKey Set By PluginActual Cache Key
openroutermodels-listplugin:openrouter:models-list
scrapflyscreenshot:abc123plugin:scrapfly:screenshot:abc123
githubrepo-info:owner/repoplugin:github:repo-info:owner/repo

Pipeline Checkpoint Cache

The StepPipelineExecutorService uses the cache for pipeline checkpoint persistence. This enables resuming failed pipelines from the last successful step.

Checkpoint Data Structure

interface CheckpointData {
stepIndex: number; // Index of the last completed step
stepName: string; // Name of the last completed step
pipelineId: string; // Pipeline plugin ID
timestamp: string; // ISO timestamp
context: unknown; // Serialized pipeline context (opaque)
completedSteps: string[]; // IDs of all completed steps
schemaVersion: number; // Version for migration (currently 4)
}

Checkpoint Lifecycle

Checkpoint Key Format

pipeline-checkpoint-{workId}-{pipelineId}

Checkpoint Configuration

SettingValueDescription
TTL24 hoursCheckpoints expire after 24 hours
Schema Version4Version for forward compatibility
SerializationsuperjsonHandles Date, Map, Set, BigInt

Schema Version Migration

When a checkpoint is loaded, the schema version is validated:

const schemaVersion = data.schemaVersion ?? 0;
if (schemaVersion !== CURRENT_CHECKPOINT_VERSION) {
// Incompatible checkpoint -- clear and restart
await this.cacheManager.del(checkpointKey);
return null;
}

If the checkpoint was saved with an older schema version, it is discarded and the pipeline restarts from scratch.

Remote Cache Proxy (Trigger.dev Workers)

Trigger.dev workers run in isolated environments without direct access to the platform's cache. The TriggerRemoteCacheModule creates a proxy that forwards cache operations over HTTP to the main API.

@Global()
@Module({})
export class TriggerRemoteCacheModule {
static forRoot(): DynamicModule {
return {
module: TriggerRemoteCacheModule,
global: true,
providers: [
{
provide: CACHE_MANAGER,
useFactory: (apiClient: TriggerInternalApiClient) => createRemoteProxy(apiClient, 'CacheManager'),
inject: [TriggerInternalApiClient]
}
],
exports: [CACHE_MANAGER]
};
}
}

This allows pipeline plugins running in Trigger.dev workers to use the same CACHE_MANAGER injection token, with operations transparently proxied to the API server.

Cache Invalidation Patterns

Automatic Invalidation

ScenarioInvalidation
Pipeline completesCheckpoint cleared via clearCheckpoint()
Schema version mismatchCheckpoint cleared on load
Plugin unloadPlugin-scoped cache cleared
TTL expiryAutomatic by cache-manager

Manual Invalidation

// Plugin clears its own cache
await context.cache.delete('expensive-computation-result');

// Pipeline checkpoint cleared after completion
await this.clearCheckpoint(workId, pipelineId);

Best Practices

  1. Use TTL for all cache entries: Always set a TTL to prevent stale data accumulation
  2. Namespace your keys: Plugin cache is already namespaced, but add sub-namespaces for complex plugins (e.g., models:, screenshots:)
  3. Handle cache misses gracefully: Always treat undefined returns as cache misses and fall back to fresh computation
  4. Use superjson for complex types: When caching objects with Dates, Maps, or Sets, use superjson.stringify() / superjson.parse()
  5. Keep cached values small: Large cache values consume memory and slow down serialization -- consider caching references (URLs, IDs) instead of full data