causal-dag-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited causal-dag-builder (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Observational analytics tempts everyone into two mistakes: assuming correlation is cause, and "controlling for everything" to launder it. Both mistakes go away when the assumed causal structure is written down first. A DAG forces the assumptions onto paper, where they can be argued with. This skill emits one, refines it as evidence arrives, and uses the back-door criterion to pick the adjustment set — instead of throwing every available variable into a regression.
experiment-result-reader instead.A causal DAG is a directed acyclic graph where nodes are variables and arrows mean "directly causes" (in the modeller's belief, not in the data). Acyclic = no variable causes itself through a loop. The DAG encodes assumptions, not facts; it is the modeller's hypothesis about the data-generating process, drawn so others can attack it. The point is not the picture. The point is that once the structure is explicit, the back-door criterion mechanically tells you which variables to condition on to estimate a causal effect — and, just as important, which variables you must not condition on.
Every triple of nodes in a DAG is one of three shapes. Conditioning rules differ for each.
| Pattern | Shape | Role of middle node | Condition on it? |
|---|---|---|---|
| Chain | X → M → Y | Mediator | No (over-adjustment: blocks the effect you're trying to measure) |
| Fork | X ← C → Y | Confounder | Yes (blocks a back-door path that would otherwise bias the estimate) |
| Collider | X → C ← Y | Collider | No (opens a spurious path that wasn't there) |
The asymmetry is the whole game. "Control for everything available" is wrong because it silently conditions on mediators and colliders. The DAG is the bookkeeping that prevents that.
To estimate the causal effect of X on Y from observational data, find a set of variables Z such that:
If such a Z exists, conditioning on Z is sufficient to recover the causal effect. If no such Z exists from observed variables, the effect is not identifiable from this data — and no amount of regression will fix that. Saying so is a real answer.
Before drawing anything, state the question in the form "does X cause Y, and by how much?" with X and Y written as observable variables. If the user said "users who visit /pricing convert higher", X = visited_pricing_page (binary), Y = converted (binary). If the user said "feature A drove the lift", X = used_feature_A, Y = the conversion metric they care about.
If X and Y can't be written down concretely, the question isn't ready for a DAG. Push back and ask the user to nail the variables before the structure.
Brainstorm the variables the modeller believes might matter. For each, label which of these it is:
The same variable can appear in multiple buckets; that's where structure comes from. Don't censor at this stage; capture the modeller's beliefs, even ones that contradict each other. The DAG arbitrates next.
Emit the DAG as Mermaid so it lives in the chat as a refinable artifact. Concrete syntax:
graph LR
intent[user_intent_to_buy] --> pageview[visited_pricing_page]
intent --> conversion[converted]
pageview --> conversionConventions:
graph LR (left-to-right) — easier to read than top-down for short DAGs.(unobserved) so downstream skills know it cannot be conditioned on.Start minimal: X, Y, and the two or three most plausible confounders. Resist the urge to draw twenty nodes.
For each path from X to Y that does not start with X → (i.e. an arrow leaving X), it's a back-door path. List them. Decide for each:
Compute the smallest Z that blocks all back-door paths without opening collider paths. That's the adjustment set. If no Z is available from observed variables, declare the effect not identifiable and stop pretending a regression will reveal it.
Downstream skills — channel-and-funnel-quality when comparing channels, metric-context-and-benchmarks when calibrating absolute rates, raw cohort-comparison queries via Clamp MCP cohorts_compare — read the DAG and the chosen Z to decide their adjustments. The DAG output should make Z explicit:
ADJUSTMENT SET (Z): {user_intent, channel}
DO NOT CONDITION ON: {plan_tier (collider), time_to_value (mediator)}
UNOBSERVED IN Z: {user_intent} — effect is point-identified ONLY if a proxy is addedIf user_intent is unobserved and no proxy exists, say so plainly. The honest answer is "we can describe the correlation; we cannot estimate the causal effect from this data."
The DAG is not done after Phase 5. As later queries surface new variables (a property in events_observed_schema, a segment in cohorts_list, an unexpected mediator from a users_journey trace), update the DAG by adding the node and the assumed arrows, then re-run Phase 4. Most real investigations cycle through 2–4 refinements before the structure is stable. The Mermaid block in the chat is the refinable artifact; replace it in place rather than appending.
User: users who visit /pricing convert 3× higher. Should we drive
more traffic to /pricing?
Agent: That's a causal claim from observational data. Let me draw
the DAG before answering.
Phase 1. Question.
X = visited_pricing_page (binary)
Y = converted (binary)
Effect of interest: does visiting /pricing cause conversion?
Phase 2. Candidate variables.
- user_intent_to_buy: causes BOTH X (high-intent users seek the
pricing page) AND Y (high-intent users convert). Likely
unobserved.
- channel: causes X (paid-search users land on /pricing more
often) and may cause Y.
- plan_tier: caused by Y (you only have a tier if you converted),
and caused by campaign. Collider — do NOT condition on.
Phase 3. Minimal DAG.
graph LR intent[user_intent_to_buy<br/>unobserved] --> pricing[visited_pricing_page] intent --> conv[converted] channel[channel] --> pricing channel --> conv campaign[campaign] --> tier[plan_tier] conv --> tier
Phase 4. Back-door walk.
Path 1: pricing ← intent → conv. Back-door via intent. Need to
condition on intent — but intent is unobserved.
Path 2: pricing ← channel → conv. Back-door via channel. Block
by conditioning on channel.
Path through plan_tier: pricing has no arrow to tier, but if we
naively control for tier we open the collider conv → tier ←
campaign, biasing the estimate. Do NOT condition on tier.
Phase 5. Adjustment set.
ADJUSTMENT SET (Z): {channel}
DO NOT CONDITION ON: {plan_tier}
UNOBSERVED IN Z: {user_intent} — back-door path via intent stays
open. The causal effect of visiting /pricing on conversion is
NOT point-identifiable from this data.
Verdict:
The "3× conversion lift" is largely intent confounding. High-
intent users seek out /pricing AND convert; the page itself
may contribute very little. Driving low-intent traffic to
/pricing will not reproduce the 3× lift, and may produce
no lift at all.
What would identify it: a randomised redirect that sends a
fraction of inbound users through /pricing regardless of
intent signal. Without that, the question is descriptive
only.User: feature A causes a 30% session-length increase. We've
controlled for time_to_value. Confidence?
Agent: Drawing the DAG first.
Phase 1. Question.
X = used_feature_A (binary)
Y = session_length (continuous, seconds)
Effect of interest: causal effect of using feature A on session
length.
Phase 2. Candidate variables.
- time_to_value: caused by feature A (users who use A reach value
faster), and causes Y (users who reach value stay longer).
This is a MEDIATOR, not a confounder.
- tenure_days: causes X (long-tenure users have explored more
features) and causes Y (long-tenure users have heavier sessions
on average). Confounder.
Phase 3. Minimal DAG.
graph LR tenure[tenure_days] --> feat[used_feature_A] tenure --> sess[session_length] feat --> ttv[time_to_value] ttv --> sess feat --> sess
Phase 4. Back-door walk.
Path 1: feat ← tenure → sess. Back-door via tenure. Block by
conditioning on tenure.
Path 2: feat → ttv → sess. This is FRONT-door (a mediated
pathway from X to Y). Conditioning on ttv blocks the part of
the effect that runs through ttv — i.e. understates the
total effect of feature A.
Phase 5. Adjustment set.
ADJUSTMENT SET (Z): {tenure_days}
DO NOT CONDITION ON: {time_to_value} — mediator; over-adjustment
will understate the effect.
Verdict:
By conditioning on time_to_value, the analysis is measuring
only the direct effect of feature A on session length, holding
time-to-value fixed. The headline "30% lift" is the residual
after blocking the main causal pathway (faster time-to-value →
longer sessions). The total causal effect is likely larger
than 30%, not smaller. Re-run without conditioning on ttv,
with tenure_days as the only control. If a decomposition into
direct vs mediated effect is wanted, that's a mediation
analysis — different skill.user_intent (unobserved) and let Phase 4 declare the effect non-identifiable. Pretending an unobserved confounder isn't there because no column exists for it is how observational claims get shipped as causal ones.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.