#410 workflow-scope report's dataRepository.getContent returns null for data written in the same workflow run
Opened by jentz · 5/22/2026· Shipped 5/22/2026
Summary
A workflow-scope report's context.dataRepository.getContent(modelType, modelId, dataName, version) returns null for every data handle that an earlier step in the same workflow run wrote via context.writeResource(spec, key, body). The data is fully persisted: the catalog row exists for the exact (type_normalized, model_id, data_name, version) the report calls with, and the raw file is on disk. swamp data get <modelName> <dataName> returns the bytes immediately. Only the in-report dataRepository read fails.
findByName and findAllForModel exhibit the same behaviour, so all three documented read methods on the workflow-scope dataRepository are affected. The report's read pipeline cannot consume data its own workflow just produced.
Environment
- swamp 20260521.221703.0-sha.3f2b75f8
- macOS arm64
- Local file datastore (default)
Reproduction
Build a model extension whose method writes multiple resources across two specs per call:
await context.writeResource("cluster", `cluster-${id}`, clusterBody); await context.writeResource("instance", `instance-${id}--${memberId}`, instanceBody);
Build a workflow-scope report that iterates
step.dataHandlesand reads each handle:for (const step of context.stepExecutions) { if (step.modelType !== "@me/my-model") continue; for (const handle of step.dataHandles ?? []) { const bytes = await context.dataRepository.getContent( step.modelType, step.modelId, handle.name, handle.version, ); // bytes === null for every handle } }
Define a workflow that runs the model and requires the report:
reports: require: - "@me/my-report"
Run:
swamp workflow run my-workflow --input '{...}'.Observe the report logs a "no bytes for X" warning for every handle, and inspect the catalog + disk to confirm the writes succeeded:
sqlite3 .swamp/data/_catalog.db \ "SELECT type_normalized, model_id, data_name, version, is_latest FROM catalog WHERE model_id='<step.modelId>'" # 44 rows, is_latest=1 on the latest version of each name ls .swamp/data/@me/my-model/<modelId>/<dataName>/<version>/raw # File exists, non-empty swamp data get my-model-name <dataName> # Returns the bytes successfully
Re-run the workflow without clearing anything —
getContentstill returns null for every handle on the second, third, ..., nth run. 5/5 consecutive runs from a clean state return 0 rows in the report.
Diagnosis
Side-channel logging inside the report confirms:
step.modelTypematches the value in the catalog'stype_normalizedcolumn exactly.step.modelIdmatches the catalog'smodel_idexactly.handle.namematches the catalog'sdata_nameexactly (cluster-<id>/instance-<id>--<member>keys, including the spec-name prefix).handle.versionmatches the catalog'sversioncolumn exactly (number, not string).handle.specNameis correctly populated; the report's iteration sees bothclusterandinstancehandles.
Despite all four arguments matching the catalog row, getContent returns null. findByName(modelType, modelId, dataName, version) returns null. findAllForModel(modelType, modelId) returns an empty array — even when the catalog has 44 rows for that exact (type_normalized, model_id).
The dataRepository instance passed to the report exposes repoDir, catalogStore, markDirty, baseDir as own properties. The read methods are on the prototype and don't throw — they just return null/empty.
Offline replay confirms the read pipeline is correct
A standalone replay that:
- reads the persisted
workflow-run-*.yaml, - extracts
step.output.dataHandlesasstep.dataHandles, - and uses a custom
dataRepository.getContentthat reads${baseDir}/${modelType}/${modelId}/${dataName}/${version}/rawviaDeno.readFile,
renders the report's CSV with 100% correct row count and content. The report code is consuming the contract correctly; the runtime's dataRepository implementation is what returns null.
Expected behaviour
context.dataRepository.getContent(modelType, modelId, dataName, version) returns the bytes for any handle in step.dataHandles (for that step) whose catalog row exists. Same for findByName and findAllForModel.
Actual behaviour
getContent, findByName, findAllForModel all return null/empty regardless of whether the data was just written or has been persisted across previous runs. The catalog query and the runtime read of dataRepository are disconnected.
Impact
Workflow-scope reports that consume upstream models writing multiple resources per method call cannot read any of those resources. The misleading-but-superficial workaround is to shell out to jq over .swamp/data/<type>/<modelId>/<dataName>/latest/raw — which sidesteps the report system entirely but loses column ordering, dedup, and other in-report features. The user-facing report is effectively non-functional on the happy path, and the workflow shows succeeded, so the failure is silent unless the report itself logs a no-bytes warning.
Affected extensions (concrete repro)
@jentz/aws-rds-inventory— model writes oneclusterresource + Ninstanceresources perlist_clusterscall.@jentz/aws-rds-inventory-csv— workflow-scope report that consumes theinstancehandles.
Both unpublished at the time of writing; loadable from local source paths via .swamp-sources.yaml. The report's smoke-test suite (which uses a synthetic dataRepository.getContent backed by an in-memory map) passes 37/37, so the bug is purely in the runtime's contract.
Extension code in PR 13
Workaround for users
jq over the on-disk JSON at .swamp/data/<modelType>/<modelId>/<handle.name>/latest/raw reproduces the report's row data without touching the report system.
Shipped
Click a lifecycle step above to view its details.
stack72 commented 5/22/2026, 3:23:00 PM
Investigation Summary
I've done extensive code analysis and attempted reproduction of this bug. Here's what I found:
Reproduction attempt: I set up a scratch repo with a multi-spec model (writing cluster + instance resources) and a workflow-scope report reading via getContent(step.modelType, step.modelId, handle.name, handle.version). Tested: single-step, multi-job, direct type execution, source-path extensions via .swamp-sources.yaml, dynamic data names with double-dashes, repeated runs. All scenarios returned correct data — 0 null results.
Code analysis: Traced every path from step write through report read. The report's dataRepository is the exact same FileSystemUnifiedDataRepository instance (this.dataRepo) used by the execution service. Both it and the step executor's instance use the same baseDir (from this.dataBaseDir). Path construction is join(baseDir, type.toDirectoryPath(), modelId, dataName, version, "raw") — identical for both.
Diagnostic Questions
Since I cannot reproduce this, I need more information from the runtime state. Could you add the following diagnostic logging inside your report's execute function and share the output?
// 1. What is the dataRepository's baseDir?
const repo = context.dataRepository as any;
context.logger.info(`baseDir: ${repo.baseDir}`);
context.logger.info(`repoDir: ${repo.repoDir}`);
// 2. For the first failing handle, what path does it construct?
const step = context.stepExecutions[0];
const handle = step.dataHandles?.[0];
if (handle) {
// Try calling getContentPath directly (it's on the prototype)
try {
const path = repo.getContentPath(step.modelType, step.modelId, handle.name, handle.version);
context.logger.info(`contentPath: ${path}`);
// Check if the file exists at that path
try {
const stat = await Deno.stat(path);
context.logger.info(`file exists: ${stat.isFile}, size: ${stat.size}`);
} catch (e) {
context.logger.info(`stat failed: ${e.message}`);
}
} catch (e) {
context.logger.info(`getContentPath failed: ${e.message}`);
}
}
// 3. Does the model directory exist?
try {
const modelDir = `${repo.baseDir}/${step.modelType}/${step.modelId}`;
context.logger.info(`modelDir: ${modelDir}`);
for await (const entry of Deno.readDir(modelDir)) {
context.logger.info(` entry: ${entry.name} (isDir: ${entry.isDirectory})`);
}
} catch (e) {
context.logger.info(`readDir failed: ${e.message}`);
}Specific questions:
- What does
repo.baseDirshow? Does it match the location whereswamp data getresolves files? - Does
getContentPaththrow (since it takesModelTypenot string), or does it return a path? If it returns a path, doesDeno.statfind the file? - Does
Deno.readDiron the model directory find entries, or does it throw NotFound? - Is there anything unusual about your
.swamp.yaml(e.g. adatastore:section)? - Are there any symlinks in the path to your repo directory?
This will help narrow down whether the issue is a path mismatch, a baseDir mismatch, or something else entirely.
jentz commented 5/22/2026, 3:49:17 PM
Swam moves fast! I will upgrade swamp and do a repo upgrade and follow the guidance. Thanks for the tunraround velocity!
jentz commented 5/22/2026, 4:17:29 PM
Thanks for the deep code analysis and the diagnostic snippet, @stack72 — the questions are clear, and I'll wire them in if the sharper repro shape below doesn't get you there first.
A more specific repro condition
I documented this in the report's source + README on PR #13 (jentz/swamp-extensions) but didn't surface it on the original issue. The failure mode appears tied specifically to a workflow model_method task that auto-creates its target model on the first run that produces data — modelType + modelName where the named model does not yet exist when the workflow starts.
When the model is pre-created via swamp model create @vendor/type name --global-arg ... before the workflow runs, the report's getContent returns the bytes correctly. When the workflow auto-creates the model in the same run that produces the data, every getContent for that model's instance handles returns null — even though the bytes are confirmed on disk under .swamp/data/<type>/<modelId>/<handle>/<version>/raw and reachable via swamp data get.
Your scenario list doesn't explicitly call out auto-create-in-workflow. Could you try the following minimal shape against your scratch repo?
name: repro
jobs:
- name: produce
steps:
- name: list
task:
type: model_method
modelType: "@vendor/multi-spec-model"
modelName: not-yet-existing # ← key: must not exist before run
methodName: list
reports:
require:
- "@vendor/multi-spec-report" # workflow-scope, reads via getContentRun once on a fresh .swamp/. If that reproduces, I'll wire your diagnostic snippet into our RDS report and capture output. If it still doesn't repro on 20260522.152735.0 (today's build, which I just upgraded to from 20260521.221703.0), I'll do the same against our real AWS workflow next time we exercise it.
What I can confirm now
.swamp.yamlhas nodatastore:section (default filesystem datastore).- No symlinks in the repo path (
/Users/mark/code/jentz/swamp-extensionsresolves to itself). - Bug documented at
aws-rds-inventory-csv/aws_rds_inventory_csv.ts:627-649andaws-rds-inventory-csv/README.md:121-153.
I'll hold on instrumenting until you've tried the sharper shape.
jentz commented 5/22/2026, 7:21:47 PM
Sounds great :D
stack72 commented 5/22/2026, 9:02:34 PM
Thanks for the sharp reproduction condition, @jentz — it narrowed the search significantly.
We traced the bug to runWorkflowReports in the workflow execution service. After a step completes, the report context was getting its modelId from a post-execution name-based definition lookup (findByNameGlobal) instead of from the step execution itself. When that lookup returned a different definition than the one used to write data, every getContent call in the report got null — exactly what you described.
We couldn't reproduce the mismatch naturally (built-in types, local extensions, and source-mounted extensions all returned the correct definition in our tests), but fault-injecting a wrong modelId into the lookup produced your exact symptoms: 6 handles visible, 6 null reads, findAllForModel empty, workflow shows succeeded.
The fix in PR #1433 carries the modelId from step execution time through the model_resolved event, so reports always use the authoritative ID that was used to write data. The definition lookup is kept only for methodArgs/globalArgs.
Could you verify this against your @jentz/aws-rds-inventory + @jentz/aws-rds-inventory-csv workflow once a build with this change is available? That would confirm the fix resolves the issue in your specific environment.
Sign in to post a ripple.