WASM / JavaScript API
Installation
Browser (ES Module)
<script type="module">
import init, { find_routes } from './node_modules/renkin/renkin.js';
await init();
const result = JSON.parse(find_routes("CC(=O)Oc1ccccc1C(=O)O", 5, 3, 0));
console.log(result.routes_found);
</script>
Browser and bundler usage
The published npm package targets browser ES modules and bundlers such as Vite,
Webpack, and Rollup. It does not support plain Node.js require() or direct
Node execution. For Node, build from source with wasm-pack --target nodejs;
see the CI-tested example.
find_routes
function find_routes(
target: string, // Target molecule SMILES
depth: number, // Maximum retrosynthetic depth
max_routes: number, // Maximum routes to return
beam_width: number // A* beam width (0 = unlimited)
): string // JSON-encoded result
WASM uses compiled-in rules and stock; this entry point cannot load external templates or stock. Use Rust or Python for that.
Input limits and validation
Public WASM search exports validate inputs before starting search. The current limits are:
| Input | Maximum |
|---|---|
| Search depth | 16 |
| Returned routes | 100 |
| Beam width and diversity slots | 10,000 |
| Candidate trace records | 50,000 |
| Target SMILES | 64 KiB |
| Element-filter text | 256 bytes |
Element filters accept the supported symbols (H, B, C, N, O, F,
Si, P, S, Cl, Br, I) as a comma-separated list. Empty tokens,
unknown symbols, and oversized values return an error. These bounds protect
the browser boundary and do not change native CLI or Python limits.
The JSON includes routes_found and routes. Each route contains steps and
building_blocks; each step identifies its target, precursors, and
template_id. Evidence and condition fields appear only when available.
Confidence values rank candidates; they are not measured yields.
find_routes_v6
find_routes_v6 keeps the policy arguments from find_routes_v5 and adds a
final candidate_trace_limit argument. It always returns a
search_diagnostics object; the candidate trace is capped at the requested
limit and does not change route selection. Existing versioned exports remain
available for compatibility.
audit_route_v2
function audit_route_v2(
content: string, // Route export JSON text (RENKIN, AiZynthFinder, Syntheseus, or SynPlanner)
format: string, // "auto" | "renkin" | "aizynthfinder" | "syntheseus" | "synplanner"
stockText: string, // "" for no stock, else one SMILES per line (.smi-style)
policy: string // "informational" | "standard" | "strict"
): string // JSON-encoded AuditRouteReport, or {"error": "..."}
This shares the CLI audit pipeline. content must be plain JSON (no gzip).
The policy changes the derived pass/fail/partial verdict, not the
findings. See the audit contract
for the report shape and policy semantics, or use the
Playground to try it locally.
Limits, locations, and cancellation
capabilities() returns machine-readable limits and accepted audit formats for
the WASM module. Call it instead of copying a numeric limit into a browser
client; it also states that the module has no network access and no
cooperative-cancellation API.
Boundary tests reject over-limit requests with resource_exhausted. Audited
steps carry occurrence_path and an atom_mapping receipt (valid, invalid,
or not_evaluable); findings may add a step_index and reason. Mapping
receipts do not change the audit verdict. See the
trusted-route guide for details.
The playground cancels by terminating and respawning its Worker; the WASM module has no cooperative cancellation API.
audit_route
The older three-argument export uses policy: "standard". New code should
use audit_route_v2.
version
Returns the RENKIN version string.
Minimal Node.js Example (CI-verified)
This CI-run example uses a locally built wasm-pack --target nodejs package,
not the published browser-targeted npm package:
// RENKIN WASM/JavaScript quickstart. Run against a
// `wasm-pack build --target nodejs` output as part of CI (see
// .github/workflows/ci.yml) so this example can never silently drift from
// the real `find_routes`/`audit_route` API.
import assert from "node:assert/strict";
import {
find_routes,
audit_route,
audit_route_v2,
capabilities,
} from "../pkg/renkin.js";
// The CI quickstart also locks the machine-readable browser boundary to the
// functions that enforce it. These calls fail before chemistry/search work.
const capability = JSON.parse(capabilities());
assert.equal(capability.schema_version, 1);
assert.equal(capability.surface, "wasm");
assert.equal(capability.network, "never");
assert.equal(capability.search.cooperative_cancel, false);
assert.equal(capability.audit.cooperative_cancel, false);
assert.deepEqual(capability.audit.accepted_formats, [
"auto",
"renkin",
"aizynthfinder",
"syntheseus",
"synplanner",
]);
assert.deepEqual(capability.audit.policies, [
"informational",
"standard",
"strict",
]);
for (const rejected of [
find_routes("CCO", capability.search.max_depth + 1, 1, 0),
find_routes("CCO", 1, capability.search.max_routes + 1, 0),
find_routes("CCO", 1, 1, capability.search.max_beam_width + 1),
]) {
assert.match(JSON.parse(rejected).error, /^resource_exhausted:/);
}
const oversizedStockLine = "C".repeat(capability.audit.max_stock_line_bytes + 1);
assert.match(
JSON.parse(audit_route_v2("{}", "auto", oversizedStockLine, "standard")).error,
/^resource_exhausted:/,
);
// A flat audit finding remains self-contained: callers can identify both the
// normalized occurrence and why forward replay was not evaluable without
// guessing from source-tool metadata or joining against `steps`.
const nonEvaluableAudit = JSON.parse(audit_route_v2(JSON.stringify({
target: "CC(=O)Oc1ccccc1C(=O)O",
routes: [{
steps: [{
rule: "ester_cleavage",
target: "CC(=O)Oc1ccccc1C(=O)O",
precursors: ["C", "O"],
template_id: "rule:ester_cleavage",
}],
building_blocks: ["C", "O"],
}],
}), "renkin", "C\nO\n", "standard"));
const nonEvaluableFinding = nonEvaluableAudit.routes[0].findings.find(
(finding) => finding.code === "forward_validation_not_evaluable",
);
assert.deepEqual(nonEvaluableFinding.occurrence_path, []);
assert.equal(nonEvaluableFinding.step_index, 0);
assert.equal(nonEvaluableFinding.reason, "missing_reaction_representation");
assert.deepEqual(nonEvaluableAudit.routes[0].steps[0].occurrence_path, []);
assert.equal(nonEvaluableAudit.routes[0].steps[0].atom_mapping.status, "not_evaluable");
assert.deepEqual(nonEvaluableAudit.routes[0].steps[0].atom_mapping.reasons, [
"missing_reaction_representation",
]);
const target = "CC(=O)Oc1ccccc1C(=O)O";
const result = JSON.parse(find_routes(target, 5, 3, 0));
console.log(`Routes found: ${result.routes_found}`);
for (const route of result.routes) {
console.log(`Route (depth ${route.depth}):`);
for (const step of route.steps) {
console.log(` ${step.target} -> ${step.precursors.join(" + ")}`);
console.log(` via ${step.rule}`);
}
}
// Audit the first found route -- same "Plan a Route" -> "Audit a Route"
// flow the playground offers, via the identical pipeline `renkin
// audit-route` uses on the CLI.
if (result.routes.length > 0) {
const route = result.routes[0];
const routeInput = JSON.stringify({
target,
routes: [{
steps: route.steps.map((s) => ({
target: s.target,
precursors: s.precursors,
template_id: s.template_id,
})),
building_blocks: route.building_blocks,
}],
});
const auditReport = JSON.parse(audit_route(routeInput, "renkin", ""));
console.log(`Audit verdict: ${auditReport.routes[0].status}`);
}