Script Runtime Environment
This document explains how scripts execute in RocketWave Pulse, including capabilities, limitations, and best practices.
How Scripts Execute
The Isolated V8 Runtime
Scripts run in an isolated V8 instance using the isolated-vm package. This provides:
- Complete Isolation - Each workflow execution gets its own V8 context
- Memory Limits - Each isolate is limited to 128MB of memory
- Timeout Protection - Scripts timeout after a configurable duration (default: 5 seconds)
- No Node.js Access - Scripts cannot access Node.js APIs, the filesystem, or network directly
┌─────────────────────────────────────────────────────────┐
│ Worker Thread │
│ ┌───────────────────────────────────────────────────┐ │
│ │ V8 Isolate (128MB) │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ Your Script Context │ │ │
│ │ │ │ │ │
│ │ │ • message object │ │ │
│ │ │ • Environment variables │ │ │
│ │ │ • Injected functions │ │ │
│ │ │ • Entity arguments │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ ↕ References │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Node.js Host Functions │ │
│ │ • AWS SDK calls (S3, Bedrock) │ │
│ │ • HTTP requests (fetch) │ │
│ │ • Pinecone operations │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Execution Flow
- Worker receives message from queue
- V8 isolate created for this execution
- Context initialized with message, variables, and script functions
- Scripts injected (print, prompt, pinecone, mastodon, stm, sportradar, pubsub)
- Workflow tree traversed - entities evaluated in order
- Entity scripts executed within the isolated context
- Results collected and isolate disposed
Async/Await Support
Scripts are automatically wrapped in an async IIFE, so you can use await at the top level:
// This works - no need for manual async wrapper
const response = await latestPromptResponse();
const result = await stmStore(['data'], response, 'file.json');
print('Stored:', result.key);
Internally, this becomes:
(async () => {
const response = await latestPromptResponse();
const result = await stmStore(['data'], response, 'file.json');
print('Stored:', result.key);
})()
What Scripts CAN Do
✅ Access Incoming Message
The message object contains the event data being processed:
print('Type:', message.type);
print('User:', message.user?.name);
print('Org:', message.organizationId);
print('Env:', message.environmentId);
✅ Access Environment Variables
Variables set in the Admin Console are injected as global constants:
// These are automatically available if set in Admin > Settings > Variables
print('API Key length:', OPENAI_API_KEY?.length);
print('Mastodon URL:', MASTODON_URL);
✅ Call Injected Functions
All script functions are available globally:
// Debugging
print('Starting processing...');
// AI/LLM
const response = await promptCallToken(apiKey, 'Hello', url);
const latest = await latestPromptResponse();
// Storage
await stmStore(['session', 'user123'], { score: 100 }, 'data.json');
const data = await stmRetrieve(['session', 'user123'], 'data.json');
// Vector database
await pineconeUpsert([{ id: '1', values: embedding }]);
const matches = await pineconeQuery(embedding, 5);
// Social media
await postToMastodon(url, token, 'Hello world!');
await postLatestPromptToMastodon();
// PubSub
await pubsubPublish('channel', JSON.stringify({ data: 'value' }));
// Sports data
const teams = await nflGetTeams();
const roster = await nflGetTeamRoster(teamId);
✅ Pass Data to Next Nodes
Set the output variable to pass data downstream:
output = {
processedAt: new Date().toISOString(),
result: await latestPromptResponse(),
metadata: { source: 'workflow' }
};
✅ Use Standard JavaScript
Most JavaScript features work:
// Arrays and objects
const items = message.items || [];
const filtered = items.filter(i => i.active);
const mapped = filtered.map(i => i.name);
// String operations
const text = message.content.toLowerCase().trim();
// Date/Time
const now = new Date();
const timestamp = now.toISOString();
// JSON
const data = JSON.parse(message.rawData);
const serialized = JSON.stringify(data);
// Error handling
try {
await riskyOperation();
} catch (error) {
print('Error:', error.message);
}
What Scripts CANNOT Do
❌ Access Node.js APIs
// These will fail - Node.js APIs not available
const fs = require('fs'); // ❌ No require
import { readFile } from 'fs'; // ❌ No imports
process.env.SECRET; // ❌ No process object
Buffer.from('data'); // ❌ No Buffer
❌ Make Direct Network Requests
// This will fail - no direct network access
fetch('https://api.example.com'); // ❌ fetch not available directly
Solution: Use injected functions that proxy through Node.js:
// ✅ Use injected functions instead
await promptCallToken(token, prompt, url); // Uses host fetch
await postToMastodon(url, token, status); // Uses host fetch
❌ Access the Filesystem
// These will fail - no filesystem access
fs.readFileSync('/etc/passwd'); // ❌ No filesystem
Solution: Use S3-based short-term memory:
// ✅ Use STM for storage
await stmStore(['data'], { key: 'value' }, 'file.json');
const data = await stmRetrieve(['data'], 'file.json');
❌ Run Forever
Scripts have a timeout (default 5 seconds):
// This will timeout
while (true) { } // ❌ Infinite loop will be killed
❌ Use Excessive Memory
Memory is limited to 128MB per isolate:
// This will crash the isolate
const huge = new Array(100000000).fill('x'); // ❌ Out of memory
❌ Access Other Workflows/Contexts
Each execution is completely isolated:
// Cannot access other workflows or global state
globalThis.sharedState = 'data'; // Only exists for this execution
Global Variables Available
All variables are injected as top-level globals, not nested under objects like env or process.
Core Variables
| Variable | Type | Description |
|---|---|---|
message | Object | The incoming event data |
output | Any | Set this to pass data to next nodes |
organizationId | String | The organization ID from the message |
environmentId | String | The environment ID from the message |
Configuration Variables
Variables defined in Admin > Settings > Variables are injected directly as globals:
// ✅ CORRECT - access variables directly
await postToMastodon(MASTODON_URL, MASTODON_ACCESS_TOKEN, analysis);
const teams = await nflGetTeams(SPORTRADAR_API_KEY);
// ❌ WRONG - there is no "env" object
await postToMastodon(env.MASTODON_URL, env.MASTODON_ACCESS_TOKEN, analysis); // Error!
process.env.MASTODON_URL // Error! No process object
Common configuration variables:
| Variable | Description |
|---|---|
MASTODON_URL | Mastodon instance URL |
MASTODON_ACCESS_TOKEN | Mastodon API token |
SPORTRADAR_API_KEY | SportRadar API key |
PINECONE_API_KEY | Pinecone vector DB key |
OPENAI_API_KEY | OpenAI API key (if configured) |
Entity Arguments
Arguments defined on workflow entities are also injected as globals:
// If entity has argument "userId" with value "{{message.user.id}}"
print('User ID:', userId); // Directly accessible
Internal Variables (Advanced)
| Variable | Type | Description |
|---|---|---|
___result___ | Any | Internal result variable for conditions |
prompt | String | The assembled prompt (Prompt entities only) |
entityModel | Object | Model configuration (Prompt entities only) |
Error Handling
Script Errors
Errors in scripts are caught and logged:
try {
const data = JSON.parse(invalidJson);
} catch (error) {
print('Parse failed:', error.message);
// Workflow continues - error is logged but not fatal
}
Timeouts
If a script exceeds the timeout, it's killed:
Error: Script execution timed out
EntityId: abc-123
EntityName: My Action
IsTimeout: true
Missing Variables
Check for undefined variables:
if (!OPENAI_API_KEY) {
print('Warning: OPENAI_API_KEY not set');
return; // Skip execution
}
Performance Considerations
Script Compilation
Scripts are pre-compiled when workers warm up, not on each execution. This means:
- First execution after deployment may be slower
- Subsequent executions use cached compilation
Async Operations
All external operations are async and cross the V8 boundary:
// Each await crosses from V8 to Node.js
const response = await promptCallToken(...); // ~100-5000ms depending on AI
const stored = await stmStore(...); // ~50-200ms for S3
const matches = await pineconeQuery(...); // ~50-300ms for Pinecone
Memory Management
The isolate is disposed after workflow completion:
- No memory leaks between executions
- Fresh context for each message
- 128MB limit per execution
Debugging Scripts
Using print()
The print() function outputs to application logs:
print('Starting script');
print('Message type:', message.type);
print('User data:', JSON.stringify(message.user));
print('Response:', await latestPromptResponse());
Checking Logs
View script output in:
- Admin Console → Processed Messages → Select a message → View logs
- CloudWatch Logs →
/ecs/rw-streaming-consumer
Common Debugging Patterns
// Log input
print('Input message:', JSON.stringify(message, null, 2));
// Check variable availability
print('Has API key:', !!OPENAI_API_KEY);
print('Variables:', Object.keys(globalThis).filter(k => k === k.toUpperCase()));
// Track execution flow
print('[1] Starting');
const result = await someOperation();
print('[2] Got result:', typeof result);
print('[3] Processing complete');
Best Practices
- Keep scripts focused - One responsibility per script
- Handle errors gracefully - Use try/catch for risky operations
- Validate inputs - Check message properties exist before using
- Use print() for debugging - Remove or reduce in production
- Avoid long-running operations - Stay well under the timeout
- Stringify objects for pubsub -
pubsubPublishrequires string arguments
Related Topics
- Scripts Overview - Function reference
- PubSub Functions - Redis messaging
- Prompt Functions - AI/LLM calls
- STM Functions - S3 storage