When agents move from single-tool demos to multi-step workflows, orchestration becomes a security and determinism problem. General-purpose languages like Python or JavaScript let agents execute arbitrary code, making it hard to enforce tool boundaries or predict what will run. SkillScript is a domain-specific language designed to solve this by making orchestration declarative and sandboxed from the ground up.
The Problem with Imperative Orchestration
Most agent frameworks (LangChain, AutoGPT) use Python or JavaScript to coordinate tools. This creates three operational risks:
- Unbounded execution: An agent can call
os.system(), open network sockets, or mutate global state between tool invocations. - Non-deterministic flows: Imperative code paths depend on runtime state, making it hard to replay or audit what an agent did.
- Tool contamination: One tool’s side effects can leak into another’s execution context, breaking isolation.
You can bolt on sandboxing after the fact (Docker containers, seccomp filters, VM snapshots), but you are fighting the language design. A declarative DSL inverts this: the language itself enforces boundaries, and the runtime only executes what the syntax permits.
How SkillScript Enforces Tool Boundaries
SkillScript separates orchestration logic from tool implementation. A skill (workflow) declares which tools to call, in what order, and how to pass data between them. The runtime interprets this declaration and executes tools in isolated contexts.
Key Design Choices
- No arbitrary code execution: The language has no
eval(), no dynamic imports, no shell escapes. You declare tool calls, not code blocks. - Explicit data flow: Variables are scoped to the skill. Tools receive inputs as arguments and return outputs as structured data. No shared mutable state.
- Sandboxed tool runtime: Each tool runs in a separate execution context. The runtime mediates all I/O, so tools cannot access the filesystem, network, or environment variables unless explicitly granted.
This is closer to a workflow engine (Temporal, Step Functions) than a scripting language. The tradeoff is expressiveness: you cannot write a loop that dynamically generates tool calls based on previous results. You declare the structure upfront.
Architecture: Compiler, Runtime, and Tool Registry
SkillScript has three components:
| Component | Responsibility | Isolation Mechanism |
|---|---|---|
| Compiler | Parses .skill files into an execution graph | Static analysis rejects invalid tool references or data flows |
| Runtime | Executes the graph, calling tools in sequence | Each tool runs in a subprocess with restricted capabilities |
| Tool Registry | Maps tool names to implementations (Python functions, HTTP endpoints, MCP servers) | Tools declare required permissions (filesystem, network, env vars) at registration time |
The compiler catches errors before execution. If a skill references a tool that does not exist, or tries to pass a string to a tool expecting an integer, the compiler rejects it. This makes orchestration failures visible at build time, not runtime.
The runtime enforces permissions. If a tool is registered with permissions: [], it cannot open files or make network requests. If it tries, the runtime kills the subprocess and returns an error to the agent.
Example: A Multi-Step Research Workflow
Here is a simplified SkillScript workflow that searches the web, extracts content, and summarizes it:
skill research_topic {
input: topic (string)
output: summary (string)
step search {
tool: web_search
args: {
query: topic,
max_results: 5
}
output: search_results
}
step extract {
tool: content_extractor
args: {
urls: search_results.urls
}
output: raw_content
}
step summarize {
tool: llm_summarizer
args: {
text: raw_content,
max_tokens: 500
}
output: summary
}
}
The runtime executes this as a directed acyclic graph. Each step runs in a subprocess. If content_extractor tries to write to disk (and was not granted filesystem permissions), the runtime terminates it and propagates an error.
Control Flow: What You Can and Cannot Do
SkillScript supports:
- Sequential steps: Execute tools in order, passing outputs to inputs.
- Conditional branches:
ifblocks that choose a path based on previous outputs. - Parallel execution: Run independent tools concurrently (the runtime schedules them).
SkillScript does not support:
- Dynamic loops: You cannot iterate over a list of unknown length and call a tool for each item. You must declare the maximum iterations upfront.
- Recursive calls: A skill cannot call itself or another skill that calls it back.
- Arbitrary functions: You cannot define helper functions inside a skill. All logic must be expressed as tool calls.
These restrictions are intentional. They make execution predictable and auditable. You can serialize a skill’s execution graph to JSON and replay it exactly. This is critical for debugging agent behavior in production.
State Management and Observability
The runtime maintains an execution log for every skill invocation. Each log entry includes:
- Tool name and arguments (sanitized to remove secrets)
- Start and end timestamps
- Output or error message
- Resource usage (CPU, memory, network bytes)
This log is append-only and immutable. You can export it to OpenTelemetry, Datadog, or a custom observability backend. Because the language is declarative, you can diff two execution logs to see exactly where behavior diverged.
State is ephemeral by default. When a skill finishes, all intermediate variables are discarded. If you need persistence, you must explicitly call a tool that writes to a database or object store. This prevents accidental state leaks between invocations.
Security Boundaries: Tool Permissions and Network Isolation
Tools declare permissions at registration time:
@tool(permissions=["filesystem:read", "network:https"])
def fetch_and_cache(url: str) -> str:
response = requests.get(url)
with open("/tmp/cache", "w") as f:
f.write(response.text)
return response.text
The runtime enforces these permissions using OS-level sandboxing (seccomp on Linux, sandbox-exec on macOS). If a tool tries to open a TCP socket but only declared network:https, the syscall fails.
You can also isolate tools by network namespace. If two tools should not communicate, run them in separate namespaces with no routing between them. This prevents a compromised tool from exfiltrating data through another tool’s network connection.
Failure Modes and Recovery
Common failure scenarios:
- Tool timeout: The runtime kills the subprocess after a configurable deadline. The skill can declare a fallback tool or propagate the error.
- Permission denied: The tool tried to access a resource it was not granted. The runtime logs the violation and terminates the tool.
- Invalid output schema: The tool returned data that does not match the declared output type. The compiler catches this if the schema is static, or the runtime rejects it if the schema is dynamic.
SkillScript does not have automatic retry logic. If you want retries, declare them explicitly:
step fetch_with_retry {
tool: http_get
args: { url: target_url }
retry: {
max_attempts: 3,
backoff: exponential
}
output: response
}
This makes retry behavior visible in the skill definition, not hidden in framework magic.
Deployment Shape
A typical deployment has three layers:
- Agent runtime: Runs the LLM and decides which skills to invoke.
- SkillScript runtime: Executes skills in sandboxed subprocesses.
- Tool implementations: Python functions, HTTP services, or MCP servers.
The agent runtime and SkillScript runtime can run in the same process or separate containers. Separating them adds latency (IPC overhead) but improves isolation. If the agent runtime is compromised, it cannot directly execute arbitrary code in the tool layer.
For high-throughput systems, run multiple SkillScript runtimes behind a load balancer. Each runtime is stateless, so you can scale horizontally. The tool registry is shared (via Redis or a service mesh), so all runtimes see the same tool definitions.
When to Use SkillScript vs. General-Purpose Orchestration
| Use SkillScript When | Use Python/JS When |
|---|---|
| You need strong isolation between tools | You need dynamic control flow (loops over unknown-length lists) |
| You want deterministic, auditable execution | You are prototyping and need maximum flexibility |
| You are building a multi-tenant agent platform (different users, different permissions) | You trust the agent and all tools implicitly |
| You need to replay or diff agent behavior | You are okay with non-deterministic execution |
SkillScript trades expressiveness for safety. If your agent workflows are predictable (search, extract, summarize), the tradeoff is worth it. If your agent needs to dynamically generate code or explore unbounded search spaces, a general-purpose language is more appropriate.
Technical Verdict
Use SkillScript when you are building production agent systems that coordinate multiple tools with different trust levels. The declarative syntax and sandboxed runtime make it easier to enforce security boundaries and debug unexpected behavior. The lack of dynamic control flow is a feature, not a bug: it forces you to make orchestration logic explicit and auditable.
Avoid SkillScript when you need maximum flexibility or are still experimenting with agent architectures. The language is opinionated, and fighting its constraints will slow you down. If you are building a single-user agent that runs in a trusted environment, the overhead of a DSL and sandboxed runtime may not be justified.
The real value is in multi-tenant or high-stakes environments where one agent’s tools should not be able to interfere with another’s, and where you need a clear audit trail of what ran and why.