---
name: graphql-in-rules
description: "GraphQL in Rubix rules: DynamicUtils vs Apollo-generated classes — decision framework, execution model, gotchas."
version: 1.1
relevant-skills: rule-scaffold, custom-code, feature-plan, feature-explain, build, module-scaffold
load: on-demand
---

# GraphQL in Rubix Rules

Rules interact with Fluent Commerce via GraphQL through `context.api()`. Two approaches exist. **The AI MUST ask the user which to use before scaffolding any rule that needs queries or mutations.**

Full API reference with code examples: `knowledge/platform/graphql-in-rules-reference.md`

---

## 1. Decision Framework — MANDATORY Question

**Before scaffolding any rule that reads or writes entities, ask:**

> "This rule needs GraphQL operations. Two approaches:
>
> **A) DynamicUtils (dynamic)** — No codegen, no generated classes. Queries built at runtime from field paths or POJOs. Best for: cross-entity queries, rapid prototyping, custom extensions without Apollo codegen setup.
>
> **B) Apollo-generated classes (typed)** — Compile-time type safety via `.graphql` files + Apollo codegen. Best for: OOTB-style rules, complex mutations with nested inputs, teams with existing codegen pipeline.
>
> Which approach do you prefer? (Or describe what the rule needs and I'll recommend.)"

### Decision Matrix

| Factor | DynamicUtils | Apollo-Generated |
|--------|:---:|:---:|
| Setup complexity | None — just import | Requires Apollo codegen (Node.js 18, `schema.json`) |
| Type safety | Runtime (Jackson / POJO) | Compile-time (generated classes) |
| Refactoring safety | Low — field paths are strings | High — compiler catches schema changes |
| Cross-entity queries | Excellent — any `queryName` + params | Requires pre-generated query per root |
| Nested input mutations | Good via `Map<String, Object>` | Excellent — builder pattern with IDE support |
| Pagination | Built-in (`DynamicConnectionQuery`) | Manual cursor loop |
| Response navigation | `JsonNode` path traversal or POJO | Typed accessor methods |
| Available in custom utility library projects | Yes — full utility library | Partial — only OOTB plugin queries |

### Quick Recommendation

- **New custom rules in existing DynamicUtils project** → DynamicUtils (consistent)
- **Extending OOTB module with existing Apollo queries** → Apollo (reuse generated classes)
- **One-off cross-entity query** → DynamicUtils (no codegen overhead)
- **Complex mutation with deeply nested input** → Either; Apollo gives IDE autocomplete
- **Team has no Apollo codegen setup** → DynamicUtils (zero infrastructure)

---

## 2. Execution Model

```
context.api().query(queryInstance)     → executes IMMEDIATELY, returns Data
context.action().mutation(mutation)    → QUEUED, executes AFTER rule returns
context.action().sendEvent(event)      → QUEUED, executes AFTER rule returns
```

**Queries are synchronous. Mutations and events are asynchronous (queued).**

- You CAN read a query result and use it to build a mutation in the same rule
- You CANNOT query an entity just created by a mutation in the same rule
- Multiple mutations execute in queue order after the rule completes

---

## 3. DynamicUtils Quick Reference

Package: `com.fluentcommerce.util.dynamic.DynamicUtils`

| Method | Purpose |
|---|---|
| `query(context, PojoClass)` | Query context entity, deserialize to POJO |
| `query(context, PojoClass, params)` | Query context entity with filters, POJO response |
| `query(context, fieldPaths)` | Query context entity, return JsonNode |
| `query(context, fieldPaths, queryName, params)` | Cross-entity query (any root) |
| `query(context, fieldPaths, queryName, params, PojoClass)` | Cross-entity query, typed POJO response |
| `queryList(context, path, PojoClass, params)` | Auto-paginated connection query |
| `mutate(context, values)` | Update context entity (auto-resolves mutation) |
| `create(context, mutationName, input)` | Create any entity |
| `executeMutation(context, name, inputType, vars, responseFields)` | Generic mutation |
| `getJsonPath(context, path)` | Single value lookup (avoid for event attrs) |

Path syntax: write `items.ref` not `items.edges.node.ref` — connections auto-expand.

---

## 4. Apollo Quick Reference

1. `.graphql` files define queries/mutations
2. Apollo codegen generates typed Java classes
3. Execute: `(MyQuery.Data) context.api().query((Query) myQuery)` — note double cast
4. Mutations: `context.action().mutation(mutation)` — queued
5. Null-check pattern: always check `data != null && data.entity() != null`, throw `RubixException` on failure
6. `Input.absent()` = field not sent. `Input.fromNullable(null)` = field sent as null.
7. `QueryUtils.connectionToList(connection)` — reflection-based connection→list helper

---

## 5. Gotchas

| Issue | Detail |
|---|---|
| `queryList` silent failure from FULFILMENT_CHOICE | Top-level queries return empty in 2ms. Use order item attributes instead. |
| `QueryDynamicVariables.valueMap()` | MUST override or all variables silently dropped. |
| `operationId()` must return non-null | Null causes NPE in Apollo HTTP interceptor. Return UUID. |
| Connection auto-expansion | Do NOT include `edges.node` in field paths — auto-inserted. |
| `context.action().mutation()` is QUEUED | Cannot read mutation result in same rule. |
| Undeclared variables rejected | Both unused-declared and undeclared-used cause silent failure. |
| Product entity special naming | Uses `variantProduct`/`standardProduct` query names, not `product`. |
| Apollo response cast | Must cast both argument `(Query)` and result `(Data)`. |
| Apollo `Input.absent()` vs `null` | Different semantics for optional mutation fields. |
| Null navigation | Every Apollo `.field()` can return null. Check each level. |
