ADR-0025: Weekly merchandising agent — first Ganimarka automation
ADR-0025: Weekly merchandising agent — first Ganimarka automation
Section titled “ADR-0025: Weekly merchandising agent — first Ganimarka automation”Status: Accepted Date: 2026-04-23
Context
Section titled “Context”With the Worker deployment target shipped (ADR-0024), the platform has somewhere to run real work. The next step has been a real choice: keep building primitives (YAML loader, memory subsystem, input validation) or ship one concrete automation that exercises everything we have under realistic constraints.
The decision to ship an automation was driven by Ganimarka becoming the concrete first customer. Selman owns the store, the store has zero sales today, and the store has no time to manage. Any automation we ship is testable against real inventory and produces visible store value the moment it runs.
The choice between candidate automations:
- Product description rewriter. Rewrites supplier copy. Concrete output. But better descriptions don’t drive sales on a brand-new store with no traffic — the bottleneck for Ganimarka is reach, not conversion.
- Weekly merchandising agent. Reads store state weekly, produces a one-page report with a recommended promotion. Concrete output. Tied to the actual question Selman has every Monday: “What should we run this week?”
- Customer support pre-draft. Webhook-triggered. But Ganimarka has zero customer messages, so this would be untestable.
The merchandising agent is the choice. It produces useful output even on a quiet store (the report is still valid; it just says “no sales last week”), it forces real architectural decisions about cron triggers and Shopify integration that any future automation will need, and it’s directly tied to a recurring decision the merchant cares about.
Decision
Section titled “Decision”Ship a weekly merchandising agent that runs Mondays at 06:00 UTC, reads three Shopify endpoints, and produces a Markdown report with inventory snapshot, recent activity, and one suggested promotion for the week.
Architecture choices, briefly
Section titled “Architecture choices, briefly”One agent, no sub-agents. The work is “read facts, write a report.” Forcing delegation here would be ceremony. Sub-agents earn their place when there’s actual work to decompose (parent reasons, sub-agent generates copy, sub-agent does layout) — we can split later if reports need richer generation.
Read-only Shopify integration. The agent cannot create campaigns, modify products, or change anything in the store. Output is a recommendation a human reviews and acts on. This is the right shape for the first automation: low blast radius, high observability, easy to reason about.
Single-tenant via custom-app token. Ganimarka generates a custom app in Shopify admin, we store the token as a Worker secret. No OAuth this session — OAuth is a separate ADR when the second merchant arrives.
Cron handler dispatches to existing async job machinery. No new orchestration shape. The cron handler in apps/worker/src/index.ts constructs a JobRecord and routes it to the existing AgentJob Durable Object. The DO’s alarm handler dispatches on JobRecord.job_type (“default” vs “merchandising”) to assemble the right runtime. Reuses everything from ADR-0024.
One DO class with a discriminated job_type. Considered separate DO classes per agent type (cleaner separation) but rejected as premature — the dispatch is one switch statement and the DO does the same shape of work in either case. Multi-class would be the right move when DO classes diverge meaningfully.
Markdown output, not structured. The report is for humans first. Structured queries (“what banner did the agent suggest 3 weeks ago”) become a real concern when there are 3 weeks of reports; we add structure then. Optimizing for grep-ability prematurely.
Reports stored in DO storage only. The DO’s existing report field on JobRecord already holds the agent’s output. No R2, no email, no UI. To read this week’s report, you GET the job (via the cron’s logged job_id) and the body comes back as a JobRecord. Not glamorous but correct. Email and a small “list recent reports” endpoint are tracked as future work.
Cost envelope
Section titled “Cost envelope”Three Shopify reads + a multi-step agent turn at Sonnet pricing is roughly $0.20 per run. The cron sets cost_budget_usd: 0.5 for safety; runaway loops fail at half a dollar. Time budget: 5 minutes.
At weekly cadence: ~$0.80/month. Negligible.
Failure mode
Section titled “Failure mode”The agent’s tools fail loud (throw on Shopify errors). The runtime catches as soft tool errors so the agent gets to retry once or report “couldn’t reach Shopify, see logs.” If everything fails, the JobRecord shows status: "failed" with a structured error. No retry, no alerting. Both are session 12+ work.
The cron firing without a Shopify token configured produces a ConfigError (fatal severity) at the DO level — the job ends in failed state, the operator sees the error in the next status poll. Better to fail loudly than silently produce a report based on no data.
Consequences
Section titled “Consequences”- First Ganimarka automation runs. Weekly. Read-only. Produces a report Selman can act on.
- Shopify package exists. Future automations (description rewriter, abandoned cart, banner generator) build on top.
- Cron trigger works. Adding new schedules is now a
wrangler.tomlline plus a branch inhandleScheduled. - The job-type dispatch pattern is established. Future agent types add themselves with a new
job_typevalue and anassemble*Runtimefactory. - No new orchestration surface. Cron + async jobs reuse the AgentJob DO from ADR-0024.
Consequences for the repo
Section titled “Consequences for the repo”- New package:
@agent-platform/shopify— read-only Admin GraphQL client. ~250 lines + 18 tests. @agent-platform/shopifyexports:createShopifyClient, three error classes (ShopifyAuthError,ShopifyRateLimitError,ShopifyApiError), domain types, the pinnedSHOPIFY_API_VERSIONconstant.- New file:
apps/worker/src/merchandising.ts— agent definition, three Shopify-read tools, runtime assembly factory. - New file:
apps/worker/src/merchandising.test.ts— smoke test withMockAdapter+ fake Shopify fetch. - Modified files:
apps/worker/src/handlers.ts(addedJobTypeandJobRecord.job_type),apps/worker/src/agent-job-do.ts(dispatch onjob_type),apps/worker/src/index.ts(added scheduled handler + acceptjob_typefrom POST /jobs body),apps/worker/wrangler.toml(cron trigger). - Tests: 21 new (18 Shopify client + 2 merchandising smoke + 1 from prior). Workspace total: 415 passed + 2 skipped.
Manual setup required after merge
Section titled “Manual setup required after merge”The agent needs three Worker secrets to function:
wrangler secret put ANTHROPIC_API_KEY # already requiredwrangler secret put SHOPIFY_ACCESS_TOKEN # NEW — from Shopify custom appwrangler secret put SHOPIFY_SHOP_DOMAIN # NEW — myshopify subdomain onlyTo create the Shopify custom app and get the access token:
- In Shopify admin, go to Settings → Apps and sales channels → Develop apps.
- Create app, name it (e.g. “Agent Platform”).
- In API credentials, configure Admin API scopes:
read_productsandread_orders. TheshopGraphQL query (used bygetShopInfo) returns basic shop info — name, email, currency, primary domain — without requiring an explicit scope. No write scopes for this session. - Install the app on your store.
- Reveal the admin API access token. This is what goes into
SHOPIFY_ACCESS_TOKEN.
SHOPIFY_SHOP_DOMAIN is the subdomain of your myshopify.com URL — e.g. for ganimarka.myshopify.com, it’s just ganimarka. Not the storefront domain (ganimarka.com).
The cron will fire next Monday at 06:00 UTC regardless of whether the secrets are set. If they’re missing the job will fail with ConfigError and the JobRecord will reflect that. To trigger a run earlier for testing, POST to /jobs with {"job_type": "merchandising", "instructions": "produce the weekly merchandising report"}.
Alternatives considered
Section titled “Alternatives considered”- Product description rewriter as the first automation. Lower business value for a no-traffic store; better engineering exercise. Rejected for now — we can come back to it as session 12 once the merchandising agent is real.
- Customer support pre-draft. Untestable on Ganimarka due to zero customer messages. Better fit for a vertical with traffic.
- Multiple DO classes per agent type. Cleaner separation but premature; one switch statement is fine until DO behaviors diverge.
- R2 for report storage. Nice property (reports persist beyond DO lifetime, can be served as static files). Premature when DO storage holds them already; revisit when “history of past reports” becomes a real feature.
- Email the report directly. Better UX. Requires an email-sending tool which we don’t have. Add when a real email integration ships.
- Structured JSON output instead of Markdown. Structured is queryable but harder to read. Markdown is for humans and renders anywhere. Add structure when querying reports becomes a thing (probably never for this one).
- Hourly or daily cadence. Weekly matches the merchandising rhythm of small e-commerce. More frequent runs would produce noise without adding signal.
- OAuth for Shopify auth. Required for multi-merchant. Not required for single-tenant Ganimarka. Defer to its own ADR with the second merchant.
- Memory of past reports so the agent knows what it suggested last week. Genuinely valuable but requires the memory subsystem (storage primitives ADR not yet shipped). Documented as the most likely next session.
What’s Next
Section titled “What’s Next”In rough order of likely value:
- Memory of past reports. Without it, every Monday’s recommendation is independent — the agent might suggest the same campaign three weeks running. Storage primitives ADR is the prerequisite. Probably next session.
- Email or Slack delivery. Right now Selman has to GET /jobs/:id to read the report. A tool that emails it changes the UX completely.
- Product description rewriter. Bulk async workload. Validates the platform under real volume. Builds on the same Shopify package.
- Banner image generation. Once descriptions are good, the next logical step is visual assets. Requires an image generation provider — likely DALL-E via a new
@agent-platform/llm-openaipackage. - OAuth + multi-tenant Shopify. When merchant #2 expresses interest.