../artifacts

Agentic Workflows for Design Engineers

Configuration patterns, failure modes, and the scripts I use to orchestrate Claude Code across complex UIs.

  • tooling
  • ai
  • workflow
  • cursor
  • claude
  • design-engineering
In this article

How I Divide the Work

I use Claude Code for orchestration and Cursor for visual iteration where sub-100ms feedback matters. Claude Code runs in a terminal and works in any editor or iTerm. On one feature, this combination brought the work down to 12 hours against my initial estimate of 40 hours.

I've spent most of my career working across design and engineering, long before the industry had a name for it: component architecture and motion design, backend contracts and interaction patterns. For a long time, that felt like an uncomfortable middle ground, too technical for design teams and too visual for engineering teams. I developed this workflow for work that needs both.

Loop and Arc

I divide the work into a visual iteration loop and an autonomous reasoning arc.

  • The Loop: I use Cursor for sub-100ms visual feedback while tuning modal springs, gallery stagger timing, and opacity transitions.

  • The Arc: I use Claude Code for multi-file refactors, reading backend contracts, and work where I'd otherwise lose 30 minutes to context-switching. It handles the structural work while I focus on the surface.

Some days I'm in Cursor for 8 hours because the work is entirely visual. Other days I'm orchestrating 5 Claude Code agents in parallel to refactor a design system.

Where this breaks: Don't use Claude Code for visual polish. It will "improve" your animation by refactoring it into something "cleaner" that looks objectively worse. I learned this after it replaced a hand-tuned expo.out curve with power2.inOut because the GSAP docs mentioned it more frequently. The code was more "correct." The interaction felt dead.

Project Rules

I put recurring constraints in project configuration to limit one-off tokens and utility functions that fragment the design system. Getting those rules right took months. I spent the first two months writing elaborate prompts for each task, but constraints drifted and mistakes repeated. Moving the invariants into CLAUDE.md improved consistency immediately.

I'm also working on a Variable Design Standard to address that fragmentation at the specification level.

Cursor: .mdc Rules

The .cursor/rules/ directory gates context by file pattern. Here's what I actually use:

yaml
# .cursor/rules/design-system.mdc
---
description: Design System & GSAP Motion Standards
globs: src/components/**/*.tsx
---
# Animation Rules
- ALWAYS use `useGSAP()` hook for lifecycle management.
- Use `gsap.utils.selector(ref)` to prevent global scope leakage.
- Easing: Default to `expo.out`. Exits must be 30% faster than entrances.

# Token Enforcement
- Spacing: Use `gap-4` (1rem) steps. No arbitrary `mt-[13px]`.
- Colors: Use semantic tokens `text-monokai-pink` over hex codes.

Why each rule exists:

  • useGSAP() handles animation cleanup. Animations created in useEffect without explicit cleanup can survive unmount. I had a modal that accumulated 47 orphaned tweens before I caught it.
  • gsap.utils.selector(ref) scopes queries to the component. Without it, Cursor generates gsap.to('.card', ...) which animates every card on the page.
  • The 30% faster exit rule came from user testing. Exits that match entrance duration feel sluggish.

What happens without config: I ran a week without .mdc files as a test. Result: 3 different easing curves across the same feature, arbitrary mt-[13px] spacing that drifted with each edit, and a GSAP timeline that used deprecated TimelineLite syntax.

Claude Code: CLAUDE.md

My CLAUDE.md includes a "What NOT to do" section for decisions I've had to undo:

markdown
# CLAUDE.md (excerpt)

## Architecture

- We build fluid, optimistic UIs.
- Server components by default. 'use client' only for interactivity.
- All animation via GSAP with `useGSAP()` hook.

## What NOT to do

- Do NOT refactor animation code unless explicitly asked.
- Do NOT add console.log statements for "debugging."
- Do NOT change easing curves. Ever.
- Do NOT "improve" accessibility by removing aria-labels you don't understand.
- Do NOT create new utility functions. Use existing patterns.

That last rule came from Claude creating formatDateRelative() when date-fns/formatDistanceToNow was already imported 3 files away.

The Workflow in Practice

I use this 3-phase pattern for features that touch both data and UI. This example is a workflow step editor for an automation builder, where users configure a multi-step sequence of actions.

Phase 1: Architect (Claude Code)

I start by having Claude Code read the backend contracts and data shape, then generate a spec for review before implementation.

bash
claude "I'm building a workflow step editor for the automation builder.
  1. Read the existing WorkflowStep type in src/types/workflow.ts
  2. Read the stepRouter in src/server/routers/steps.ts
  3. Identify the mutation contracts I need to respect
  4. Generate specs/WORKFLOW_STEP_EDITOR.md with:
     - Component hierarchy
     - State management approach for optimistic updates
     - Edge cases (validation, error states, concurrent edits)
  DO NOT write code. Spec only."

The "DO NOT write code" instruction keeps implementation behind spec review. Without it, Claude will scaffold 12 files before you've validated the approach. Reading the existing contracts gives the spec an API to work against.

Actual output (truncated):

markdown
# Workflow Step Editor Specification

## Existing Contracts (from stepRouter)

- createStep: { workflowId, type, config } → Step
- updateStep: { stepId, config } → Step (optimistic, revalidates on error)
- reorderSteps: { workflowId, stepIds[] } → void

## Component Hierarchy

- StepEditor (container, manages local state)
  - StepTypeSelector (dropdown, triggers config schema change)
  - StepConfigForm (dynamic form based on step.type)
  - StepActions (save, delete, duplicate)

## State Management

- Local state for draft changes (useReducer)
- Optimistic update on save via tRPC useMutation
- Rollback to server state on 409 Conflict (concurrent edit)

## Edge Cases

- Empty config: Show placeholder, disable save
- Type change mid-edit: Confirm discard of existing config
- Network failure: Toast + retry button, preserve draft

Phase 2: Painter (Cursor)

Open the spec in Cursor. Highlight it. Cmd+I to open Composer.

"Scaffold the StepEditor component based on this spec. Use GSAP for the panel transitions when switching step types. Reference design-system.mdc. The transition should feel responsive, user should never wait for animation to complete before interacting."

Actual output (StepEditor.tsx):

This excerpt omits the project-specific types, reducer, and cache utility. It is not a standalone component.

tsx
'use client'

import { useGSAP } from '@gsap/react'
import gsap from 'gsap'
import { useRef, useReducer } from 'react'
import { api } from '@/lib/trpc'

export function StepEditor({ step, workflowId }: StepEditorProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const [draft, dispatch] = useReducer(stepReducer, step)

  const updateStep = api.steps.updateStep.useMutation({
    onMutate: async (newData) => {
      // Optimistic update
      await utils.steps.getStep.cancel()
      const previous = utils.steps.getStep.getData({ stepId: step.id })
      utils.steps.getStep.setData({ stepId: step.id }, newData)
      return { previous }
    },
    onError: (err, _, context) => {
      // Rollback on conflict
      if (context?.previous) {
        utils.steps.getStep.setData({ stepId: step.id }, context.previous)
      }
    },
  })

  useGSAP(
    () => {
      const q = gsap.utils.selector(containerRef)
      gsap.fromTo(
        q('.config-panel'),
        { opacity: 0, x: 12 },
        { opacity: 1, x: 0, duration: 0.25, ease: 'expo.out' },
      )
    },
    { scope: containerRef, dependencies: [draft.type] },
  )

  return (
    <div ref={containerRef} className='flex flex-col gap-4'>
      <StepTypeSelector
        value={draft.type}
        onChange={(type) => dispatch({ type: 'SET_TYPE', payload: type })}
      />
      <div className='config-panel'>
        <StepConfigForm config={draft.config} type={draft.type} />
      </div>
      <StepActions
        onSave={() => updateStep.mutate(draft)}
        disabled={!draft.config}
      />
    </div>
  )
}

The output follows the .mdc rules: useGSAP(), a scoped selector, and expo.out. The transition lasts 0.25s. I spent 20 minutes on the reducer logic instead of 2 hours fixing animation fundamentals.

Phase 3: Auditor (Claude Code)

Once the visual work is ready, I ask Claude Code to verify the integration.

bash
claude "Run the test suite for the StepEditor feature.
  If any layout tests or accessibility checks fail:
  1. Read the error logs
  2. Fix the component code
  3. Verify the fix
  Repeat until all tests pass."

Where this broke: Claude Code "fixed" an accessibility failure by removing an aria-label it didn't understand. The axe-core check passed, but screen reader users lost context entirely.

The fix was adding to CLAUDE.md:

markdown
- Do NOT remove aria attributes to fix a11y errors. Ask first.

Failure Modes

Context Window Exhaustion. Claude Code's 200K tokens sounds massive until you're refactoring across 40 files. Around file 30, it starts "forgetting" constraints from CLAUDE.md. I've seen it reintroduce TimelineLite syntax it had correctly avoided for the first 20 files. Mitigation: Batch refactors at 15-20 files. Fresh context each batch.

The Refactor Trap. Ask Claude to fix a bug in Button.tsx and it will "notice" that Card.tsx has similar patterns that could be "improved." Before you've reviewed the diff, it has touched 8 files. Mitigation: Explicit scoping in every prompt. "Do NOT modify any other files." Verify diff before accepting.

Import Hallucinations. Cursor pulls from training data, not your package.json. I've had it import from lucide-react when the project uses heroicons. The component renders, the icons are wrong, and you don't notice until production. Mitigation: Add explicit package constraints to .mdc.

The Confidence Problem. I haven't found a way to make the tools' confidence useful as a review signal. A subtle race condition and a typo fix get the same tone. I still read every line of the code.

Subagents and Scripts

I use subagents for specialized work within Claude Code, including domains where I don't want to be the expert. Their constraint is report, don't fix. Claude Code will "help" by implementing changes unless I prevent it.

I learned this after letting an a11y subagent "fix issues autonomously" across the codebase. It touched 47 components; 12 got worse. Now every subagent prompt ends with "Do NOT modify files" until I've reviewed the report.

I use Context7 MCP for documentation, including GSAP 3.13, to avoid the deprecated ScrollMagic patterns Claude draws from 2023. A11y audits stay scoped to changed files with explicit "Do NOT modify" instructions. I review the reports before changes land.

The two scripts I reach for most:

spec: Generate a Feature Spec

bash
#!/bin/bash
# spec - Generate a feature specification

FEATURE_NAME=$1
[ -z "$FEATURE_NAME" ] && echo "Usage: spec <feature-name>" && exit 1

claude "Create a specification for: $FEATURE_NAME
  Include: data model changes, API routes, component hierarchy,
  state management, edge cases.
  Output to specs/${FEATURE_NAME}.md
  DO NOT write implementation code."

variant: Ship A/B Variants

bash
#!/bin/bash
# variant - Create a feature-flagged variant of a component

COMPONENT=$1
VARIANT_NAME=$2
[ -z "$COMPONENT" ] || [ -z "$VARIANT_NAME" ] && \
  echo "Usage: variant src/components/Button.tsx dark-mode" && exit 1

claude "Create a feature-flagged variant of $COMPONENT.
  1. Read the existing component
  2. Create ${COMPONENT%.tsx}.${VARIANT_NAME}.tsx with the variant
  3. Update original to check flag and render variant conditionally
  4. Use existing pattern in src/lib/flags.ts
  Variant is additive—do NOT modify behavior when flag is off."

I use this to keep interaction experiments behind a flag. Turning an experiment off without a deploy requires a flag that can be changed at runtime.

What Still Requires Judgment

Six months into this workflow, the hardest part is deciding when to stop iterating. An animation could be 2% smoother or the code marginally cleaner; the tools make another pass easy. I still have to decide when the work is ready to ship.

I've watched designers adopt these tools and still break things because constraining output to existing primitives requires understanding the system. Reviewing the result means knowing when the code is right and when it merely compiles.

Project configuration remains reactive: it catches problems after generation. I'm working on a Variable Design Standard to address the fragmentation between design intent and shipped code through enforceable specifications. The aim is to reduce the drift across handoffs and tooling, and make those corrective configuration rules less necessary over time.