Skip to main content
from metalworks import Metalworks — one object you construct, and everything hangs off it. This page is the full reference: every public method, its signature, what it takes, and what it returns. New to the library? Start with the walkthrough; come here when you need the exact surface. The one rule that shapes every return type: nothing is invented. Every claim carries an EvidenceRef that resolves to a real quote or web finding on the report’s evidence list. When a method can’t ground something, it drops it or marks the result partial — it does not fill the gap with plausible text.

Constructing the client

The common cases:
Provider refs route by namespace: anthropic, openai, google/gemini are native; anything else (openrouter/..., meta-llama/...) routes through an OpenAI-compatible endpoint. To point at a local or custom endpoint, construct an OpenAIChatModel(base_url=..., api_key_env=...) and pass it as chat=.

Research

.research(...)

Runs the demand pipeline and returns a frozen Research bundle. If you’re inside a project, the run is automatically persisted to .metalworks/runs/<report_id>/. Casual use (no project) leaves no footprint.

.plan(prompt)

Walks the planner end-to-end (taking the recommended answer at each decision) and returns a ResearchBrief you can inspect, edit, and pass straight back into .research(brief).

The Research bundle

research() returns a frozen Research. The demand report is on .demand; .evidence is the flat, resolvable evidence list every downstream method’s EvidenceRefs point at. Key fields you’ll read on .demand: Each InsightCluster carries rank, claim, demand_score, distinct_author_count, mention_count, signal, and quotes (verbatim ResolvedCitations with text, source_url, source/source_name, author_hash, engagement).

The stage methods

Each method below runs on a finished research() bundle (or a bare DemandReport). They’re the Research → Design → Build → Launch → Grow arc. All read from the same report, so every output traces back to the same evidence.

Research stage

positioning returns a Dunford-style wedge + price hypothesis, with partial=True + a caveat when there’s no defensible wedge. (The competitive map lives inside landscape() below — there’s one “what exists today” door now, not two.)

The validation loop

The discovery loop — frame an idea, weigh demand against what already exists, get an honest verdict. See the validation loop for the full picture.
  • ideate (idea-first) sharpens a raw idea into a testable hypothesis + a ResearchBrief; ideate_from_evidence (evidence-first) surfaces a report’s forks — candidate wedges, else top clusters — as grounded IdeaSketches to pick from.
  • landscape is the full “what exists today”: the nested competitor_map (direct/adjacent/status-quo rivals, each gap cited and each tagged with the clusters it competes for) plus an empirical existing-solutions scan (real shipped products, matched to demand clusters).
  • assess is the heart: a deterministic GO / PIVOT / NO-GO gap over demand × landscape (the LLM only writes the rationale). PIVOT carries a pivot_target — a real fork id to aim at. A partial landscape never yields a hard GO.
  • validate runs the loop headlessly: ideate → demand → landscape → assess, looping on PIVOT. It pulls the corpus once and reuses it for every in-corpus pivot — a fresh pull happens only if a pivot leaves the corpus.

Design stage

design authors a grounded-but-directional DesignSystem (an aesthetic direction + a SAFE/RISK choice per dimension), read from a real browser teardown of competitor sites where available and honest about its grounding tier.

Build stage

build_spec maps demand to a feature list (each feature tied to ≥1 real quote; ungrounded ones are dropped), picks the surface with a one-line rationale when surface="auto" (the default), and sketches a feature-grounded screen skeleton. scaffold writes a cite-or-die build harness under dest and returns the paths written. metalworks writes the spec, not the product — your coding agent builds from the scaffold.
scaffold raises ValueError if spec.report_id doesn’t match the research bundle.

Distribution stage

channel_strategy routes the report’s real named entities + signals into test→focus channel experiments (D2) — every routing_signal traces to a real corpus entity. distribution_requirements emits the embedded loops + conversion surface distribution designs INTO the product as BUILD requirements (D3): a LoopRequirement per embedded_loop channel (loop kind → concrete build requirements grounded in the channel’s signal) plus a ConversionSurfaceRequirement for the destination the channels point at — feed it to build_spec(..., distribution_requirements=...). channel_assets drafts channel-SHAPED, drafting-only assets per channel (D4) — one ChannelAsset per selected channel, shaped to its surface (Product Hunt tagline + maker comment; Show HN title + first comment; X thread; LinkedIn carousel); demand/factual claims are grounded, persuasive hooks free, platform invariants enforced — it never posts. data_asset projects the report into a corpus-derived data report (D5), the on-brand flagship asset: a deterministic ranking of the report’s clusters carrying their REAL distinct-author / mention counts, real permalinks, and a verbatim quote per row; kind picks the framing (complaint_index | feature_ranking | state_of). distribution_plan sequences those channels into a campaign (D7): pushes (the spike-cadence channels placed into concentrated launch moments, their timing READ from a deterministic playbook table — Product Hunt 12:01am PT Tue/Wed, Show HN Tue-Thu 8-10am, … — never invented hours; one all-day-attention channel per day, never Product Hunt + a big HN push the same day; pre-launch warming → push week → a 30-day post step) + streams (the compounding-cadence channels run continuously). channel_metrics closes the loop (D8): one ChannelMetric per channel — its success_metric (what “worked”) + its instrumentation (how to track it), read deterministically by surface_type; record a ChannelResult per channel after the push and pass them as prior_results to build_channel_strategy / plan_distribution to re-rank the next push (winners rise). geo is the GEO / LLM-citability stream (D6): participation targets (real threads to engage, from the report’s permalinks), citability probes (conversational queries to test you’re cited, from the cluster claims), and answer-first answer briefs (grounded — each evidence_refs resolves against report.evidence, stat_anchors carry the cluster’s real counts, ungrounded answers dropped). distribution_engage is the participation/execution arm (D9) — the one channel metalworks can OPERATE rather than merely plan: it takes one of geo’s participation_targets (a real thread) and drafts a disclosed, founder-voiced reply for that exact thread, reusing the Reddit reply machinery + the single voice system’s invariants (no upvote ask, native-first, no AI tells), then runs the shared honesty gate (heuristic_check) over it — returning a ParticipationReply (the draft + the compliance verdict + requires_human / posting_gated, both always true). All are drafting only — nothing posts; a human posts a participation reply through the gated mw.reddit.post(...) path.

.deps

The resolved dependency container (chat, embeddings, corpus, reader…). The escape hatch for calling the raw stage functions yourself — e.g. build_positioning_brief(mw.deps, report) — without rebuilding the providers by hand.

.reddit — Reddit surfaces

Reads are zero-key; the rate limiter is shared across all calls on one client.
.inbox and .post are the authenticated surfaces. .post is gated: it runs the deterministic compliance check first and refuses on a block verdict (returning a failed PostResult), and every attempt — blocked or sent — is appended to ~/.metalworks/post-log.jsonl. It needs REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET and a previously connected account. See Reddit engagement.

.discovery — find threads, draft replies

run is the full loop (search → filter → generate → compliance-gate) and returns draft Opportunity objects — it never posts. filter and generate are the building blocks if you want to drive the loop yourself.
Persona.background must be authentic — fabricated personas and invented backstories are prohibited by the usage policy.

Exceptions

All inherit from MetalworksError, which carries an optional fix hint and docs_url. Import them from metalworks.errors.

See also