Skip to content

Hybrid Gene Development Guide

Hybrid genes extend the Rotifer safety model to support controlled network access. Unlike Wrapped genes (pure functions) or Native genes (WASM-only), Hybrid genes can call external APIs — but only through a Network Gateway that enforces domain whitelisting, rate limiting, timeouts, and response size caps.

This guide walks you through creating, testing, and publishing a Hybrid gene.

FidelityNetworkUse Case
WrappedNonePure data transformation, text processing
HybridGateway-controlledLLM APIs, database queries, embeddings
NativeNoneCPU-intensive WASM computation

Use Hybrid when your gene needs to call an external service (OpenAI, Anthropic, Supabase, etc.) but you still want the protocol’s safety guarantees.


Terminal window
rotifer init my-hybrid-gene --fidelity Hybrid

This creates a project with a pre-configured phenotype.json that includes the network block:

{
"name": "my-hybrid-gene",
"domain": "general",
"fidelity": "Hybrid",
"version": "0.1.0",
"inputSchema": { "type": "object" },
"outputSchema": { "type": "object" },
"network": {
"allowedDomains": ["api.example.com"],
"maxTimeoutMs": 30000,
"maxResponseBytes": 1048576,
"maxRequestsPerMin": 10
}
}

You can also convert an existing gene to Hybrid:

Terminal window
rotifer wrap my-gene --fidelity Hybrid --domain text.analyze

Edit phenotype.json to declare the exact domains your gene needs:

{
"network": {
"allowedDomains": ["api.openai.com", "*.supabase.co"],
"maxTimeoutMs": 30000,
"maxResponseBytes": 1048576,
"maxRequestsPerMin": 10
}
}
ParameterTypeDefaultDescription
allowedDomainsstring[][]Domains the gene may call. Supports exact match and *.domain.com wildcards
maxTimeoutMsnumber30000Per-request timeout in milliseconds
maxResponseBytesnumber1048576Maximum response body size (1 MiB default)
maxRequestsPerMinnumber10Rate limit — requests per 60-second sliding window
  • Exact match: api.openai.com allows only api.openai.com
  • Wildcard: *.supabase.co allows abc.supabase.co and supabase.co
  • Forbidden domains (rejected at publish):
    • localhost, 127.x.x.x, 0.0.0.0, ::1
    • Private IPs: 10.x.x.x, 172.16–31.x.x, 192.168.x.x

Hybrid genes receive a gatewayFetch function via the second argument. Use it instead of the global fetch:

export async function express(
input: { query: string },
ctx?: { gatewayFetch?: typeof fetch }
): Promise<{ answer: string }> {
if (!ctx?.gatewayFetch) {
throw new Error("gatewayFetch is required for Hybrid genes");
}
const response = await ctx.gatewayFetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: input.query }],
}),
});
const data = await response.json();
return { answer: data.choices[0].message.content };
}

When your gene calls gatewayFetch:

  1. Domain check — the URL’s hostname is validated against allowedDomains. If blocked, a DOMAIN_BLOCKED error is thrown immediately.
  2. Rate check — if the gene has exceeded maxRequestsPerMin in the current 60-second window, a RATE_LIMITED error is thrown.
  3. Timeout — the request is aborted after maxTimeoutMs with a TIMEOUT error.
  4. Response cap — if the response body exceeds maxResponseBytes, it is truncated and a truncated: true flag is set.
CodeMeaning
DOMAIN_BLOCKEDURL hostname not in allowedDomains
RATE_LIMITEDExceeded maxRequestsPerMin
TIMEOUTRequest exceeded maxTimeoutMs
RESPONSE_TOO_LARGEResponse body exceeded maxResponseBytes
NETWORK_ERROROther fetch failure

Terminal window
rotifer compile my-hybrid-gene

For Hybrid genes, the compiler preserves the fidelity: "Hybrid" flag and embeds the network configuration into the IR custom sections. The output shows the allowed domains:

✓ Compiled to Rotifer IR
Fidelity: Hybrid
Network: api.openai.com, *.supabase.co
IR Hash: sha256-abc123...

Terminal window
rotifer publish my-hybrid-gene

The publish command runs two Hybrid-specific validations before authenticating:

If network.allowedDomains is missing or empty:

✗ [E0055] Hybrid gene must declare at least one allowed domain
in network.allowedDomains
Fix: Add "network": { "allowedDomains": ["api.example.com"] }
to phenotype.json

If any domain resolves to localhost or a private IP:

✗ [E0056] Forbidden domain in network.allowedDomains: "localhost"
(localhost/private IP not allowed)
Fix: Remove private/localhost domains before publishing

Create an agent that includes your Hybrid gene:

Terminal window
rotifer agent create my-agent --genes my-hybrid-gene
rotifer agent run my-agent --input '{"query": "What is Rotifer?"}'

The agent runtime automatically:

  1. Detects fidelity: "Hybrid" in the gene’s phenotype
  2. Creates a NetworkGateway with the gene’s network configuration
  3. Injects gatewayFetch into the express() call
  4. Logs gateway stats with --verbose

The Rotifer docs-assistant pipeline demonstrates a 4-gene Hybrid composition:

query-parser (Wrapped)
doc-retrieval (Hybrid) → OpenAI Embeddings + Supabase pgvector
answer-synthesizer (Hybrid) → Claude / OpenAI LLM
source-linker (Wrapped)

Each Hybrid gene declares only the domains it needs:

GeneAllowed Domains
doc-retrievalapi.openai.com, *.supabase.co
answer-synthesizerapi.anthropic.com, api.openai.com

Only declare the domains your gene actually calls. Each domain in allowedDomains is a trust surface — keep it small.

"allowedDomains": ["api.openai.com"]

Handle gateway errors explicitly — don’t let network failures crash your gene:

try {
const res = await ctx.gatewayFetch(url, options);
// ...
} catch (err) {
if (err.code === "RATE_LIMITED") {
return { answer: "Rate limited — please retry.", error: true };
}
throw err;
}

Never hardcode API keys in gene source. Use environment variables:

const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY not set");

Design genes to support multiple LLM providers via environment variables:

const provider = process.env.ROTIFER_LLM_PROVIDER || "openai";
const endpoint = provider === "claude"
? "https://api.anthropic.com/v1/messages"
: "https://api.openai.com/v1/chat/completions";

Tune maxTimeoutMs and maxResponseBytes for your use case:

Use CaseTimeoutResponse Size
LLM chat completion30s1 MiB
Embedding API10s256 KiB
Database query5s512 KiB