ProcessFlow Sandbox Guardrails
Document information
- Canonical URL:
/docs/02_automations-and-processflow/40_sandbox-guardrails - Version:
2026-06-17 - Tags:
processflow,reference,sandbox,security
ProcessFlow runs step code inside a sandbox so automations execute safely on shared infrastructure. Guardrails block unsafe patterns—direct command execution, unrestricted networking, environment mutation, and raw database drivers—while still allowing practical work through injected platform services. Use this guide as a checklist before publishing workflows that read files, call APIs, or transform tenant data.
What the sandbox protects
The sandbox prevents operations that can bypass tenant isolation or destabilize the runtime: direct OS commands, uncontrolled filesystem access, unsafe network behavior, and unvetted module loading. When a step is blocked, treat the response as design feedback—move the behavior to an approved service, validate inputs earlier, and keep each step focused on one safe action.
How guardrails are enforced
Rules apply at two layers: validation when you save or validate a snippet, and restriction when the step runs. If either layer detects a blocked construct or invalid configuration, the step fails with a guardrail or security error instead of running partially.
Design snippets around approved service objects (tf.tenantDb, tf.connector, tf.api, tf.keystore, tf.files, …) or their legacy root aliases (tenantDb, integration, api, …) rather than low-level language primitives.
Each run also carries sandbox capabilities (logging, data-access, connector, api, file, keystore, llm, …). Calling a service without its capability throws at runtime (for example Connector capability is not enabled). Inspect tf.capabilities when a step may run with a restricted profile. See Process sandbox tf API reference.
What is restricted by default
The sandbox blocks patterns including:
- Dynamic execution —
eval,create_function, variable function calls,child_process, and similar - Shell and process control —
exec,system,proc_open,process.exit, and related APIs - Low-level networking — direct
fetchto arbitrary hosts, raw sockets, unauthorizedcurl_*calls - Direct database drivers — importing or using raw SQL client libraries instead of
tenantDb - Module loading in tenant steps —
require,import,include, andinclude_once(tenant-scoped steps;/roothas separate rules) - Raw server globals — reading request environment outside injected context (
http_headers,request_method, …) - Unsafe structure — defining custom functions or classes inside snippets when not supported by your runtime profile
- Environment mutation — writing to
process.envor equivalent
Replace blocked primitives with injected services and structured return objects.
Use approved services instead of blocked primitives
| Need | Use |
|---|---|
| Outbound HTTP / internal APIs | tf.api or api |
| Integration connectors (sync) | tf.connector.execute |
| Integration connectors (async queue) | tf.integration.executeAsync, getStatus, getResult |
| Tenant database access | tf.tenantDb or tenantDb (not raw db except /root) |
| Secrets | tf.keystore or keystore |
| Uploaded or tenant files | tf.files, read/write, and related helpers |
| LLM, email, notifications | tf.llm, tf.email, tf.notification |
| Hashing / HMAC for webhooks | tf.utils.sha256, tf.utils.hmacSha256 |
See Process environment variables and Process sandbox tf API reference for the full injected surface.
type StepInput = { customer_id?: string };
type StepResult =
| { success: true; data: { customer_id: string; has_customer: boolean } }
| { success: false; error: { code: string; message: string } };
async function runGuardrailSafeStep(
input: StepInput,
api: { get(url: string): Promise<{ success: boolean; data?: { entity?: { entity_id?: string } } }> }
): Promise<StepResult> {
if (!input.customer_id) {
return { success: false, error: { code: "VALIDATION_ERROR", message: "customer_id is required" } };
}
const response = await api.get(`/api/v1/entities?id=${encodeURIComponent(input.customer_id)}`);
if (!response.success) {
return { success: false, error: { code: "API_ERROR", message: "Failed to load customer entity" } };
}
return {
success: true,
data: {
customer_id: input.customer_id,
has_customer: Boolean(response.data?.entity?.entity_id),
},
};
}Test with explicit tenant and auth context
Validate behavior before wiring logic into production. Two useful checks:
1. Run the process end-to-end (confirms snippet + guardrails + orchestration):
curl -X POST "https://<YOUR_DOMAIN>/api/v1/processflow?action=execute-process" \
-H "X-API-Key: <API_KEY>" \
-H "X-Tenant-ID: <TENANT_ID>" \
-H "Content-Type: application/json" \
-d '{
"process_id": "<PROCESS_ID>",
"input_data": {
"customer_id": "<ENTITY_ID>"
},
"async": false
}'
{
"success": true,
"data": {
"execution_id": "exec_5f2c90aa",
"status": "completed"
}
}
2. Probe a dependency API directly (isolates contract issues from sandbox policy):
curl -X GET "https://<YOUR_DOMAIN>/api/v1/customers/<ENTITY_ID>" \
-H "X-API-Key: <API_KEY>" \
-H "X-Tenant-ID: <TENANT_ID>" \
-H "Content-Type: application/json"
Authoring rules that improve pass rate
- Return structured results:
{ success: true, data: … }on success;{ success: false, error: { code, message } }on failure. - Validate
process_inputbefore use; never trust user-supplied tenant identifiers over injectedtenant_id. - Use parameterized queries through
tenantDb— avoid SQL string concatenation. - Keep file paths tenant-relative; reject directory traversal (
.., absolute paths outside tenant storage). - Prefer asynchronous processing for long-running work instead of pushing synchronous steps toward timeout limits.
Configuration, schemas, and operational limits
| Setting | Guidance |
|---|---|
execution_timeout | 1 to 86 400 seconds (24 hours). Default per step is 1 hour unless configured otherwise. |
memory_limit | Per-step memory input; default 64 MB. The JS/TS VM runner does not hard-cap process memory to this value today—treat it as a planning hint. |
| Retries | Bounded by executor policy (typically up to 3 retries for transient failures). |
When input_schema or output_schema are set, field names must match [a-zA-Z_][a-zA-Z0-9_]* and types must be one of: string, integer, int, float, double, boolean, bool, array, object, null, mixed.
For platform-wide defaults and ceilings, see Platform limits.
If your step is rejected
Common causes:
- Blocked functions or unsafe class usage
require/import/includein tenant-scoped steps- Invalid syntax or snippet structure
execution_timeoutor schema definitions outside allowed bounds- Direct networking or database access outside approved services
Diagnose quickly: identify whether the denial involves filesystem, command execution, network, or unmanaged dependencies. Replace that action with an approved service, rerun with the same input, and return structured error codes from your step so operators do not need raw logs.
Remediation: replace low-level primitives with injected services, simplify the snippet, and split complex logic into smaller policy-compliant steps.