From soda-gql-skills
Generate GraphQL fragments and operations with type-safe syntax
How this skill is triggered — by the user, by Claude, or both
Slash command
/soda-gql-skills:gql-scaffoldThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill generates type-safe GraphQL fragments and operations from schema introspection. Tagged template syntax is the default; callback builder (options-object path) is only used for `$colocate` or programmatic field control.
This skill generates type-safe GraphQL fragments and operations from schema introspection. Tagged template syntax is the default; callback builder (options-object path) is only used for $colocate or programmatic field control.
First, detect the soda-gql project configuration:
!bun ${CLAUDE_PLUGIN_ROOT}/scripts/detect-project.ts
The output includes:
found: Whether a soda-gql project was detectedconfigPath: Path to the config fileschemas: Schema names and their file pathsoutdir: Output directory for generated fileshasLsp: Whether LSP is availableIf found: false, inform the user:
No soda-gql project detected. Make sure you have a
soda-gql.config.{ts,js,mjs}file and have runbun install.
Exit the skill.
Use lsp-cli when available for structured JSON type information, or fall back to reading raw schema files.
hasLsp: true)First, verify the binary is available:
soda-gql-lsp-cli schema
If the command succeeds, it returns the type list as JSON:
{ "types": [{ "name": "string", "kind": "string" }] }
Then fetch details for each relevant type:
soda-gql-lsp-cli schema <TypeName>
Multi-schema projects: When the detect-project output schemas has multiple keys, pass --schema <schemaName> to every lsp-cli command:
soda-gql-lsp-cli schema --schema <schemaName>
soda-gql-lsp-cli schema <TypeName> --schema <schemaName>
Type detail JSON output shapes by kind:
{ "name": "string", "kind": "string", "fields": [{ "name": "string", "type": "string", "args": [{ "name": "string", "type": "string" }] }] }{ "name": "string", "kind": "UNION", "members": [{ "name": "string" }] }{ "name": "string", "kind": "ENUM", "values": [{ "name": "string" }] }{ "name": "string", "kind": "INPUT_OBJECT", "fields": [{ "name": "string", "type": "string" }] }hasLsp: false OR binary fails)If hasLsp: false or soda-gql-lsp-cli schema fails (binary not found, non-zero exit, etc.), fall back to reading the raw schema files directly:
# For each schema file in schemas object
Read <schema-file-path>
This gives you the GraphQL schema definition including:
Parse $ARGUMENTS to understand what the user wants to query. If unclear or empty, use AskUserQuestion:
Question: "What would you like to query?"
Examples:
Based on the user's intent, determine whether to create:
If uncertain, use AskUserQuestion to clarify.
CRITICAL: Tagged template is the default. Only use callback builder (options-object path) for $colocate or programmatic field control.
Is $colocate or programmatic field control needed?
├─ YES → Callback builder (options-object path)
└─ NO → Tagged template ✓ (default)
Tagged template handles all common cases including aliases, directives, and fragment spreads:
Simple fields:
const userFields = gql.default(({ fragment }) =>
fragment("UserFields", "User")`{ id name email }`(),
);
With aliases:
const userFields = gql.default(({ fragment }) =>
fragment("UserFields", "User")`{
userId: id
displayName: name
}`(),
);
With directives:
const userFields = gql.default(({ fragment }) =>
fragment("UserFields", "User")`($includeEmail: Boolean!) {
id
name
email @include(if: $includeEmail)
}`(),
);
Fragment → Fragment spread:
const extendedFields = gql.default(({ fragment }) =>
fragment("ExtendedUser", "User")`{
...${userBasicFields}
createdAt
updatedAt
}`(),
);
Static metadata:
const componentFragment = gql.default(({ fragment }) =>
fragment("UserCard", "User")`{ id name }`({
metadata: { component: "UserCard", cacheTTL: 300 },
}),
);
Callback metadata:
const componentFragment = gql.default(({ fragment }) =>
fragment("UserCard", "User")`($userId: ID!) { id name }`({
metadata: ({ $ }: { $: { userId: string } }) => ({
cacheKey: `user:${$.userId}`,
}),
}),
);
Is $colocate or programmatic field control needed?
├─ YES → Callback builder (options-object path) — see $colocate below
└─ NO → Tagged template ✓ (default)
Tagged template handles simple fields, aliases, directives, and fragment spreads:
Simple operation:
const getUserQuery = gql.default(({ query }) =>
query("GetUser")`($id: ID!) {
user(id: $id) {
id
name
email
}
}`(),
);
Operation → Fragment spread (tagged template):
const getUserQuery = gql.default(({ query }) =>
query("GetUser")`($id: ID!) {
user(id: $id) {
...${userFields}
}
}`(),
);
Multiple fragment spreads (tagged template):
const getUserQuery = gql.default(({ query }) =>
query("GetUser")`($id: ID!) {
user(id: $id) {
...${userCardFragment}
...${userMetaFragment}
}
}`(),
);
Use callback builder only when $colocate is needed. $colocate applies prefix-based field aliasing for multi-fragment operations — it enables multiple components to each receive their own slice of a query result without field name collisions. This requires the FieldsBuilder callback context (the f() and $ helpers) which is not available in tagged templates.
const userPageQuery = gql(({ query, $colocate }) =>
query("UserPage")({
variables: `($userId: ID!)`,
fields: ({ f, $ }) => $colocate({
userCard: {
...f("user", { id: $.userId })(() => ({
...userCardFragment.spread(),
})),
},
}),
})(),
);
"Fragments declare requirements; operations declare contract"
Fragment with variables:
const userConditional = gql.default(({ fragment }) =>
fragment("UserConditional", "User")`($includeEmail: Boolean!) {
id
name
email @include(if: $includeEmail)
}`(),
);
Operation spreading fragment with variables (tagged template):
const getUserQuery = gql.default(({ query }) =>
query("GetUser")`($id: ID!, $includeEmail: Boolean!) {
user(id: $id) {
...${userConditional}
}
}`(),
);
Use these templates based on the decision tree outcome:
// Import path depends on project's outdir config (from detect-project output)
// Examples: "@/graphql-system", "./src/graphql/generated", etc.
import { gql } from '<outdir-path>';
export const <name>Fragment = gql.default(({ fragment }) =>
fragment("<Name>Fragment", "<TypeName>")`{
<field1>
<field2>
<nestedField> {
<subField1>
}
}`(),
);
// Import path depends on project's outdir config (from detect-project output)
import { gql } from '<outdir-path>';
export const <name>Fragment = gql.default(({ fragment }) =>
fragment("<Name>Fragment", "<TypeName>")`($<var1>: <Type1>!, $<var2>: <Type2>) {
<field1>
<field2> @include(if: $<var1>)
<field3> @skip(if: $<var2>)
}`(),
);
// Import path depends on project's outdir config (from detect-project output)
import { gql } from '<outdir-path>';
import { <baseFragment> } from './<baseFragmentFile>';
export const <name>Fragment = gql.default(({ fragment }) =>
fragment("<Name>Fragment", "<TypeName>")`{
...${<baseFragment>}
<additionalField1>
<additionalField2>
}`(),
);
// Import path depends on project's outdir config (from detect-project output)
import { gql } from '<outdir-path>';
import { <fragment1> } from './<fragmentFile>';
export const <name>Query = gql.default(({ query }) =>
query("<OperationName>")`($<var1>: <Type1>!, $<var2>: <Type2>) {
<rootField>(<arg1>: $<var1>, <arg2>: $<var2>) {
...${<fragment1>}
<field1>
<nestedField> {
<subField1>
}
}
}`(),
);
// Import path depends on project's outdir config (from detect-project output)
import { gql } from '<outdir-path>';
export const <name>Mutation = gql.default(({ mutation }) =>
mutation("<OperationName>")`($<inputVar>: <InputType>!) {
<mutationField>(input: $<inputVar>) {
<resultField1>
<resultField2>
}
}`(),
);
Use only when $colocate (prefix-based field aliasing) is required.
// Import path depends on project's outdir config (from detect-project output)
import { gql } from '<outdir-path>';
import { <fragment1> } from './<fragmentFile>';
export const <name>Query = gql(({ query, $colocate }) =>
query("<OperationName>")({
variables: `($<var1>: <Type1>!)`,
fields: ({ f, $ }) => $colocate({
<componentSlice>: {
...f("<rootField>", { <arg>: $.<var1> })(() => ({
...<fragment1>.spread(),
})),
},
}),
})(),
);
Only gql is exported from the generated runtime. The import path depends on the project's outdir config (from detect-project output).
Runtime import (all syntax styles):
// Import path depends on project's outdir config (from detect-project output)
// Examples: "@/graphql-system", "./src/graphql/generated", etc.
import { gql } from '<outdir-path>';
Fragment imports (when spreading):
import { fragmentName } from './fragments';
// or
import { frag1, frag2 } from './fragments';
Use AskUserQuestion to determine where to write the generated code:
Question: "Where should I create the generated code?"
Options:
**/fragments.{ts,tsx}**/operations.{ts,tsx}Use the Write tool to create or append the generated code:
// If creating new file
Write <file-path> <generated-code>
// If appending to existing file
// 1. Read existing file
// 2. Append generated code
// 3. Write updated content
Run validation to ensure the generated code is correct:
Step 0: Run lsp-cli diagnostics (when available)
If soda-gql-lsp-cli is available (verified successfully in Step 3), run diagnostics on the generated file before typegen:
soda-gql-lsp-cli diagnostics <generated-file-absolute-path>
The output is a JSON array of diagnostic objects:
[{ "message": "string", "line": "number", "column": "number", "severity": "string" }]
[]) means no issues — proceed to Step 1.severity: "Error" indicate real problems — treat these as validation failures and proceed to Step 13's fix loop."Warning", "Hint") can be noted but do not block validation.soda-gql-lsp-cli is not available, skip this sub-step gracefully and proceed to Step 1.Step 1: Run typegen
bun run soda-gql typegen
Expected output:
Step 2: Run typecheck
bun typecheck
Expected output:
If validation fails, analyze the error and fix the code:
Error: Field does not exist on type
Error: Field 'fieldName' does not exist on type 'TypeName'
Analysis:
Fix:
Error: Unknown type referenced
Error: Unknown type 'TypeName'
Analysis:
Fix:
Error: Missing required variable
TypeError: Variable 'varName' is not defined in operation
Analysis:
Fix:
Error: Invalid fragment spread
Error: Fragment spread target type mismatch
Analysis:
Fix:
Track validation attempts:
On Success (after validation passes):
✅ GraphQL code generated successfully!
Generated files:
- <file-path> — <Fragment/Operation name>
Summary:
- Type: <Fragment/Query/Mutation>
- Syntax: <Tagged Template/Callback Builder>
- Lines added: <N>
Next steps:
- Import and use the generated code in your components
- Run `gql:doctor` to verify overall project health
- Use `gql:guide` for help with advanced patterns
On Failure (after 3 retries):
❌ Code generation failed after 3 validation attempts.
Last error:
<error message>
Generated code location:
- <file-path> (may contain errors)
Suggested fixes:
<specific fix suggestions based on error type>
Manual steps:
1. Review the generated code at <file-path>
2. Check the schema file for correct types and fields
3. Use `gql:guide` for syntax help
4. Run `bun run soda-gql typegen` to see detailed errors
Quick reference for syntax selection:
| Feature Needed | Syntax Required |
|---|---|
| Simple field selection | Tagged template ✓ |
| Field aliases | Tagged template ✓ |
| Fragment → Fragment spread | Tagged template ✓ (with ${...}) |
| Operation → Fragment spread | Tagged template ✓ (with ${...}) |
| Variables in directives | Tagged template ✓ (fragment declares vars) |
| Static metadata | Tagged template ✓ |
| Callback metadata | Tagged template ✓ |
| $colocate pattern | Callback builder (options-object path) |
| Union member selection (inline fragments) | Tagged template ✓ |
Key Principle:
When generating code for union types, use standard GraphQL inline fragments in tagged templates:
const searchQuery = gql.default(({ query }) =>
query("Search")`($term: String!) {
search(term: $term) {
__typename
... on User {
id
name
}
... on Post {
id
title
}
}
}`(),
);
Note: Union types use standard GraphQL inline fragment syntax (... on TypeName). Always include __typename for type discrimination.
Metadata is passed as an argument to the tagged template call — static objects and callbacks both work. See the examples in Section 6 (Fragment Definition).
$colocate is the only case requiring callback builder syntax — see Section 6 (Callback Builder: $colocate Pattern) for the full explanation and example.
Before completing this skill, ensure:
User: "Create a fragment for User with id, name, email"
Process:
Generated code:
import { gql } from '<outdir-path>';
export const userBasicFragment = gql.default(({ fragment }) =>
fragment("UserBasic", "User")`{ id name email }`(),
);
User: "Create a query to get a user by ID using the userBasic fragment"
Process:
Generated code:
import { gql } from '<outdir-path>';
import { userBasicFragment } from './fragments';
export const getUserQuery = gql.default(({ query }) =>
query("GetUser")`($id: ID!) {
user(id: $id) {
...${userBasicFragment}
}
}`(),
);
User: "Create a mutation to update a task's title and completion status"
Process:
Generated code:
import { gql } from '<outdir-path>';
export const updateTaskMutation = gql.default(({ mutation }) =>
mutation("UpdateTask")`($taskId: ID!, $title: String, $completed: Boolean) {
updateTask(id: $taskId, input: { title: $title, completed: $completed }) {
id
title
completed
}
}`(),
);
npx claudepluginhub whatasoda/soda-gql-skills --plugin soda-gql-skillsGuides writing GraphQL operations (queries, mutations, subscriptions, fragments) with best practices for naming, variables, directives, and data fetching patterns.
Covers GraphQL schema design, resolvers, DataLoader for N+1 prevention, federation, subscriptions, and client integration with Apollo/urql.
Builds production-ready GraphQL servers with schema design, DataLoader resolvers, WebSocket subscriptions, and field-level authorization for Node.js and Python.