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.

ProcessFlow sandbox model showing restricted operations, approved platform services, and secure tenant-scoped execution paths.

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 executioneval, create_function, variable function calls, child_process, and similar
  • Shell and process controlexec, system, proc_open, process.exit, and related APIs
  • Low-level networking — direct fetch to arbitrary hosts, raw sockets, unauthorized curl_* calls
  • Direct database drivers — importing or using raw SQL client libraries instead of tenantDb
  • Module loading in tenant stepsrequire, import, include, and include_once (tenant-scoped steps; /root has 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.env or equivalent

Replace blocked primitives with injected services and structured return objects.

Use approved services instead of blocked primitives

NeedUse
Outbound HTTP / internal APIstf.api or api
Integration connectors (sync)tf.connector.execute
Integration connectors (async queue)tf.integration.executeAsync, getStatus, getResult
Tenant database accesstf.tenantDb or tenantDb (not raw db except /root)
Secretstf.keystore or keystore
Uploaded or tenant filestf.files, read/write, and related helpers
LLM, email, notificationstf.llm, tf.email, tf.notification
Hashing / HMAC for webhookstf.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_input before use; never trust user-supplied tenant identifiers over injected tenant_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

SettingGuidance
execution_timeout1 to 86 400 seconds (24 hours). Default per step is 1 hour unless configured otherwise.
memory_limitPer-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.
RetriesBounded 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 / include in tenant-scoped steps
  • Invalid syntax or snippet structure
  • execution_timeout or 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.

See also