External Tools Landscape
What third-party tools/runtimes the substrate dispatches to and how they auth.
External Tools Landscape — operator runbook
Status: Phase A — §3.1 (schema) and §3.2 (seed importer) are shipped. §3.3 (staleness scanner) and §3.4 (benchmark linkage validator) are not yet implemented. This runbook documents what is live today and flags the pending pieces.
The external tools landscape is a curated catalog of AI-for-science tools,
platforms, agents, and benchmarks — the v1 “AI Tools Landscape” — modeled as
the external_tool artifact type (SPEC-083). Browse, filter, and search via
standard polymorphic verbs; no bespoke /forge/landscape route logic is
required on the substrate side.
System overview
external_tool artifacts (SPEC-083 §2)
→ schema: src/scidex_substrate/schema_registry/default_schemas/external_tool.json
→ seeded by: scripts/seed_external_tools.py (23 entries, SPEC-083 §3.2)
→ browsed via: scidex.list type=external_tool [filter=...]
→ searched via: scidex.search type=external_tool query=...
→ lifecycle: draft → active → deprecated → archived
staleness scanner (SPEC-083 §3.3) ← NOT YET SHIPPED
→ weekly job re-fetches homepage_url
→ flags entries: last_verified >90 days OR homepage 404
→ writes stale_flag signal (aggregation=replace)
benchmark linkage validator (SPEC-083 §3.4) ← NOT YET SHIPPED
→ external_tool_benchmark_refs_resolve
→ fires on scidex.create / scidex.update for external_tool type
→ rejects benchmark_scores[] entries whose benchmark_ref does not resolve
Artifact type
external_tool is a reference catalog entry. It is distinct from the
tool type (SPEC-002), which is an executable tool the substrate dispatches.
An external_tool artifact describes a third-party product; the substrate
catalogues it but does not run it.
Required fields
| Field | Type | Notes |
|---|---|---|
name |
string | Display name (max 200 chars) |
homepage_url |
URI | Canonical product URL |
category |
enum | See categories below |
open_source |
boolean | Whether source code is publicly available |
Optional fields
| Field | Type | Notes |
|---|---|---|
specializations |
string[] | Free-form tags (e.g. "protein_folding") |
pricing |
enum | free / freemium / paid / enterprise / unknown |
license |
string | SPDX id or short description |
github_url |
URI | VCS repository URL |
last_verified |
date | ISO date of last successful homepage fetch |
benchmark_scores[] |
array | See §Benchmark linkage |
short_description |
string | ≤280 chars; used for catalog cards and embedding |
long_description |
string | Markdown extended description |
Categories
| Value | Meaning |
|---|---|
agent_platform |
General-purpose AI assistant or agent platform |
literature |
Paper search, evidence synthesis, citation tools |
structure |
Protein / macromolecular structure prediction |
design |
De novo protein or molecule design |
imaging |
Microscopy analysis, cell segmentation |
lab_automation |
Liquid handling, ELN, protocol automation |
benchmarking |
Scientific benchmarks and evaluation frameworks |
data_repo |
Curated databases and data repositories |
other |
Does not fit another category |
Seed importer
scripts/seed_external_tools.py creates the 23 entries carried over from v1’s
AI Tools Landscape. It is idempotent (duplicate creates are soft-skipped via
HTTP 409).
Dry run (default)
python scripts/seed_external_tools.py
Output shows all 23 entries that would be written; nothing is changed.
Apply (write to substrate)
SCIDEX_SUBSTRATE_URL=http://127.0.0.1:8200 python scripts/seed_external_tools.py --apply
Expected output:
{
"total": 23,
"inserted": 23,
"skipped": 0,
"errors": []
}
Re-running after the first apply yields "inserted": 0, "skipped": 23 — that
is correct and expected behavior.
Verify seed was applied
curl -s http://127.0.0.1:8200/api/scidex/list \
-H 'content-type: application/json' \
-d '{"type":"external_tool","limit":30}' \
| python3 -m json.tool
The total field in the response must be ≥23 after one importer run.
Adding a new entry
Via HTTP (preferred for one-off additions)
SUBSTRATE_URL=http://127.0.0.1:8200
curl -s -X POST "$SUBSTRATE_URL/api/scidex/create" \
-H 'Content-Type: application/json' \
-d '{
"type": "external_tool",
"title": "My Tool",
"id": "extool-my-tool",
"content": {
"name": "My Tool",
"homepage_url": "https://mytool.example.com",
"category": "literature",
"open_source": false,
"pricing": "freemium",
"last_verified": "2026-05-15",
"short_description": "One-paragraph summary ≤280 chars."
}
}' | python3 -m json.tool
ID convention
IDs must match the pattern extool-{slug} where slug is the tool name
lowercased with non-alphanumeric runs replaced by hyphens. For example:
"AlphaFold 2" → extool-alphafold-2. The seed importer uses the same
_slug() function; follow the same convention to avoid duplicates.
Adding entries to the seed importer
If the entry should persist across re-deployments, add it to
scripts/seed_external_tools.py using _make_artifact(). Add it to the
appropriate section list (LITERATURE, STRUCTURE, etc.) and ensure the
ALL_ENTRIES concatenation at the bottom includes it. Update the module
docstring to reflect the new totals.
Refreshing a stale entry
When last_verified is more than 90 days old, or when the homepage URL
changes, update the entry via scidex.update:
SUBSTRATE_URL=http://127.0.0.1:8200
ARTIFACT_ID=extool-alphafold-2
curl -s -X POST "$SUBSTRATE_URL/api/scidex/update" \
-H 'Content-Type: application/json' \
-d "{
\"type\": \"external_tool\",
\"id\": \"$ARTIFACT_ID\",
\"content\": {
\"last_verified\": \"2026-05-15\"
}
}" | python3 -m json.tool
scidex.update bumps the version and preserves prior content per SPEC-009.
Partial updates are merged; only the fields you provide are changed.
Clearing a stale_flag signal manually
Until the staleness scanner (§3.3) ships, set or clear stale_flag signals
directly:
# Mark an entry as verified (clear stale_flag)
curl -s -X POST "$SUBSTRATE_URL/api/scidex/signal" \
-H 'Content-Type: application/json' \
-d "{
\"type\": \"external_tool\",
\"id\": \"$ARTIFACT_ID\",
\"signal\": \"stale_flag\",
\"value\": false
}" | python3 -m json.tool
Retiring an entry
Use lifecycle transitions to deprecate an entry that has been superseded or is no longer maintained.
Deprecate (tool still exists but is superseded)
curl -s -X POST "$SUBSTRATE_URL/api/scidex/signal" \
-H 'Content-Type: application/json' \
-d '{
"type": "external_tool",
"id": "extool-esmfold",
"lifecycle_transition": "deprecated"
}' | python3 -m json.tool
Then link to the successor entry:
curl -s -X POST "$SUBSTRATE_URL/api/scidex/link" \
-H 'Content-Type: application/json' \
-d '{
"from_type": "external_tool",
"from_id": "extool-esmfold",
"link_type": "superseded_by",
"to_type": "external_tool",
"to_id": "extool-esm3"
}' | python3 -m json.tool
Archive (tool is defunct)
curl -s -X POST "$SUBSTRATE_URL/api/scidex/signal" \
-H 'Content-Type: application/json' \
-d '{
"type": "external_tool",
"id": "extool-old-tool",
"lifecycle_transition": "archived"
}' | python3 -m json.tool
Archived entries are excluded from browse/filter by default unless
include_archived=true is passed.
Valid lifecycle transitions
| From | To |
|---|---|
draft |
active |
active |
deprecated |
active |
archived |
deprecated |
archived |
Browsing and filtering
Browse uses scidex.list — no bespoke route required:
# All active entries
curl -s http://127.0.0.1:8200/api/scidex/list \
-H 'content-type: application/json' \
-d '{"type":"external_tool","limit":50}' \
| python3 -m json.tool
# Filter by category
curl -s http://127.0.0.1:8200/api/scidex/list \
-H 'content-type: application/json' \
-d '{"type":"external_tool","filter":{"category":"structure"},"limit":20}' \
| python3 -m json.tool
# Filter by open_source
curl -s http://127.0.0.1:8200/api/scidex/list \
-H 'content-type: application/json' \
-d '{"type":"external_tool","filter":{"open_source":true},"limit":50}' \
| python3 -m json.tool
Prism renders /forge/landscape as a generic list page using these same
scidex.list calls — no per-category HTML is required.
Semantic search
curl -s http://127.0.0.1:8200/api/scidex/search \
-H 'content-type: application/json' \
-d '{"type":"external_tool","query":"protein structure prediction","limit":10}' \
| python3 -m json.tool
Embeddings are built from name + "\n" + short_description per the schema’s
search.embedding_recipe. Ensure short_description is populated for
meaningful semantic-search results.
Benchmark linkage (SPEC-083 §3.4) — NOT YET SHIPPED
Per the spec, a validator external_tool_benchmark_refs_resolve will fire on
create/update and reject benchmark_scores[] entries whose benchmark_ref
does not resolve to a real benchmark artifact (SPEC-023).
Current state: benchmark_scores[] entries are stored as-is without
cross-reference validation. Until §3.4 ships:
-
Any string is accepted in
benchmark_ref. -
Track benchmark refs by convention: use
benchmark:<id>form. -
The
links.benchmarksarray (separate frombenchmark_scores) can hold typed links tobenchmarkartifacts and is validated by the standard link resolver.
Staleness scanner (SPEC-083 §3.3) — NOT YET SHIPPED
Per the spec, a weekly scheduled job will re-fetch each entry’s homepage_url,
flag entries whose last_verified is >90 days old or whose homepage returns
404, and write a stale_flag signal.
Current state: staleness detection is manual. Until §3.3 ships, operators can identify stale entries with this SQL query:
-- entries whose last_verified is more than 90 days ago
SELECT
id,
(content ->> 'name') AS name,
(content ->> 'homepage_url') AS homepage_url,
(content ->> 'last_verified') AS last_verified,
(content ->> 'category') AS category
FROM artifacts
WHERE type = 'external_tool'
AND lifecycle_state = 'active'
AND (content ->> 'last_verified')::date < NOW() - INTERVAL '90 days'
ORDER BY (content ->> 'last_verified')::date;
Refresh those entries using the scidex.update flow described above.
Common operational queries
Count entries by category
SELECT
content ->> 'category' AS category,
COUNT(*) AS entry_count
FROM artifacts
WHERE type = 'external_tool'
AND lifecycle_state = 'active'
GROUP BY category
ORDER BY entry_count DESC;
Find all open-source entries
SELECT
id,
content ->> 'name' AS name,
content ->> 'category' AS category,
content ->> 'github_url' AS github_url
FROM artifacts
WHERE type = 'external_tool'
AND (content ->> 'open_source')::boolean = true
ORDER BY content ->> 'category', content ->> 'name';
Find entries with stale_flag signals
SELECT
a.id,
a.content ->> 'name' AS name,
s.value AS stale_flag_value,
s.created_at AS flagged_at
FROM artifacts a
JOIN signals s ON s.artifact_id = a.id AND s.signal_type = 'stale_flag'
WHERE a.type = 'external_tool'
AND (s.value::text)::boolean = true
ORDER BY s.created_at;
Look up a specific entry
curl -s http://127.0.0.1:8200/api/scidex/get \
-H 'content-type: application/json' \
-d '{"type":"external_tool","id":"extool-alphafold-2"}' \
| python3 -m json.tool
Check version history of an entry
SELECT version, content_hash, created_at
FROM artifact_versions
WHERE artifact_id = 'extool-alphafold-2'
ORDER BY version DESC
LIMIT 10;
Relationship to Prism
Prism (SPEC-003) renders /forge/landscape as a generic list page over
external_tool artifacts. It uses only standard scidex.list and
scidex.search calls with category / specialization / pricing filters — no
per-entry or per-category bespoke HTML.
If the landscape page is missing entries, verify:
-
The seed importer ran with
--applyand reported zero errors. -
The substrate is running and accessible at the expected URL.
-
The
lifecycle_statefilter is not excluding active entries.
Access control
Initial policy (per SPEC-083 §5): any authenticated actor may
scidex.create(type=external_tool). Promotion to “featured” status (when
implemented) will require a Senate vote via a separate signal — the same
signal/comment machinery all artifacts use.
Operators with the scidex-curator role (SPEC-020 §10) can update or retire
entries without the normal comment/review cycle. Bulk operations (e.g. a
category-wide last_verified refresh) should be run as the scidex-curator
principal.