How I use plans, skills, specialist agents, /fleet, MCP, code generation, and independent validation on a real TypeScript middleware integration.
Most developers begin with GitHub Copilot CLI by asking it to explain a file, generate a command, or fix an error.
That was my starting point too.
But while developing Shopify applications on Azure, I found a more valuable pattern: use Copilot CLI as the coordinator for the entire engineering workflow not only as the tool that writes code.
Here is one real example.
We need a TypeScript headless middleware API that serves Shopify storefront experiences and consumes Shopify webhooks. After receiving a webhook, the middleware applies enterprise business rules, updates internal APIs and database systems, and publishes normalized events to Azure Event Hubs for other consumers.
The interesting part is not whether Copilot can generate an HTTP handler. It can.
The real test is whether it can help us design, divide, implement, validate, and review the complete workflow without hiding the difficult integration decisions.
The feature I asked Copilot CLI to help build
The architecture has two related paths:
Buyer or storefront
→ TypeScript headless middleware
→ Shopify Storefront API
Shopify webhook
→ webhook authentication and deduplication
→ Azure Service Bus queue
→ TypeScript event worker
→ enterprise business rules
→ operational database and transactional outbox
→ internal API delivery + Azure Event Hubs publication
→ downstream enterprise consumers
I use Service Bus between webhook receipt and processing because Shopify expects the endpoint to respond quickly. Event Hubs is the downstream event stream, not the place where an unverified webhook is accepted directly.
That distinction became part of the Copilot plan before any code was generated.
Pattern 1: I begin with /plan, not code generation
I start Copilot CLI in plan mode:
copilot --plan
Then I describe the outcome and the boundaries:
Plan a TypeScript Shopify headless middleware feature on Azure.
The middleware must:
- call the Shopify Storefront API for buyer-facing requests;
- receive orders/create and orders/updated webhooks;
- verify authenticity before trusting the body;
- deduplicate repeated deliveries and tolerate out-of-order events;
- acknowledge accepted webhooks quickly;
- enqueue processing through Azure Service Bus;
- apply enterprise order-routing rules in a worker;
- update an internal Order API and operational database;
- publish a normalized OrderChanged event to Azure Event Hubs;
- use managed identity for Azure resources;
- include correlation IDs, telemetry, retries, and failure-path tests.
Do not implement yet. Identify trust boundaries, token types, state
transitions, consistency risks, deployment changes, and rollback options.
This forces the conversation beyond “create a webhook endpoint.”
The plan must answer questions such as:
- What happens if Shopify sends the webhook twice?
- What happens if an older update arrives after a newer one?
- What happens if the internal API succeeds but Event Hubs publication fails?
- Which token belongs in the browser, middleware, or background worker?
- Which failures should be retried, dead-lettered, or rejected permanently?
Pattern 2: Instructions define the permanent engineering rules
I keep repository-wide expectations in:
.github/copilot-instructions.md
For this project, those instructions include:
- Use TypeScript strict mode and validate data at every external boundary.
- Keep Shopify payloads, Shopify client objects, Azure SDK types, and database rows out of domain logic.
- Verify Shopify webhook HMAC before parsing or trusting the payload.
- Treat webhooks as duplicated and potentially out of order.
- Never expose Admin API or private Storefront tokens to browser code.
- Prefer managed identity for Service Bus, Event Hubs, Key Vault, and other Azure resources.
- Separate business decisions from network and database side effects.
- Use a transactional outbox when a database change and event publication must behave as one logical operation.
- Require stable error codes, safe structured telemetry, tests, and a focused diff.
Instructions define the standards that should survive across sessions and developers.
Pattern 3: Skills turn our procedures into reusable workflows
I use project skills for work that the team will repeat:
.github/skills/
├── shopify-webhook-consumer/
│ ├── SKILL.md
│ └── fixtures/
├── storefront-token-review/
│ └── SKILL.md
├── azure-event-publisher/
│ └── SKILL.md
└── integration-validation/
├── SKILL.md
└── scripts/
The shopify-webhook-consumer skill tells Copilot how our repository authenticates a delivery, extracts its webhook ID, maps the Shopify payload into an internal command, and tests duplicates and ordering.
The storefront-token-review skill checks the credential boundary:
- A public Storefront access token can be used in a public client when that is the intended architecture.
- A private Storefront token stays in the server-side middleware.
- Server-side Storefront requests caused by buyer traffic forward the required buyer IP header.
- Admin access tokens used for background Admin API operations remain server-side, scoped minimally, encrypted at rest, and associated with the correct shop installation.
- Application secrets and encryption keys are retrieved through the approved secret-management boundary rather than being committed or logged.
The skill does not contain a live token. It contains the repeatable procedure for handling tokens correctly.
The server-side Storefront adapter can use Shopify’s official TypeScript client while keeping the private token behind a secret-provider interface:
import { createStorefrontApiClient } from "@shopify/storefront-api-client";
export async function createPrivateStorefrontClient(
shop: InstalledShop,
secrets: StorefrontSecretProvider,
) {
const privateAccessToken = await secrets.getPrivateToken(shop.id);
return createStorefrontApiClient({
storeDomain: `https://${shop.myshopifyDomain}`,
apiVersion: SHOPIFY_STOREFRONT_API_VERSION,
privateAccessToken,
});
}
The adapter adds Shopify-Storefront-Buyer-IP for server-side calls initiated by buyer traffic. The buyer IP is derived through the application’s trusted-proxy policy, not accepted blindly from an arbitrary forwarded header. Public Storefront clients are created separately so browser code cannot accidentally receive the private token.
Pattern 4: Specialist agents investigate before /fleet implements
I do not ask one general agent to make every decision.
For this feature, I use specialist agents with separate contexts:
- Shopify integration agent: webhook authentication, API version, scopes, sessions, Storefront clients, and payload contracts.
- Azure integration agent: Service Bus, Event Hubs, managed identity, retry policy, dead-letter behavior, and telemetry.
- Domain agent: order state transitions and enterprise routing decisions.
- Test agent: fixtures, contract tests, failure injection, duplicate delivery, and ordering scenarios.
- Security reviewer: read-only review of tokens, secrets, logs, permissions, and untrusted input.
The Shopify agent can explore the existing adapter without filling the main conversation with every library detail. The Azure agent can inspect the infrastructure code independently. Their conclusions return to the main agent, which owns the combined plan.
Pattern 5: /fleet executes independent parts in parallel
After I approve the plan, I use:
/fleet implement the approved Shopify middleware plan
Copilot can divide independent work among sub-agents:
Sub-agent 1 → Shopify webhook ingress and contract mapping
Sub-agent 2 → Service Bus consumer and domain orchestration
Sub-agent 3 → database outbox and Event Hubs publisher
Sub-agent 4 → tests, fixtures, and validation commands
I monitor the work with:
/tasks
I do not use /fleet merely because it exists. The webhook ingress and Event Hubs adapter can be developed independently after their contracts are agreed. A database migration that must land before its repository code is inherently ordered and should remain sequential.
Parallelism is useful only after the dependencies are understood.
Pattern 6: Generated code stops at explicit boundaries
The webhook handler should be intentionally small. In simplified TypeScript, the generated shape looks like this:
export async function receiveShopifyWebhook(
request: Request,
dependencies: WebhookIngressDependencies,
): Promise<Response> {
const delivery = await dependencies.shopifyWebhookVerifier.verify(request);
if (!delivery.authenticated) {
return new Response("Unauthorized", { status: 401 });
}
const alreadyAccepted = await dependencies.deliveryStore.exists(
delivery.webhookId,
);
if (alreadyAccepted) {
return new Response(null, { status: 200 });
}
const command = dependencies.webhookMapper.toOrderCommand(delivery);
await dependencies.queue.acceptOnce({
messageId: delivery.webhookId,
correlationId: delivery.webhookId,
body: command,
});
await dependencies.deliveryStore.recordAccepted(delivery.webhookId);
return new Response(null, { status: 200 });
}
The real implementation uses the Shopify library or framework authentication helper selected by the repository. The application does not reimplement cryptography casually.
The handler does four things: authenticate, deduplicate, map, and enqueue. It does not wait for three enterprise systems to respond while Shopify is waiting for an acknowledgement.
The worker owns the slower orchestration:
export async function processOrderWebhook(
command: ShopifyOrderCommand,
dependencies: OrderWorkflowDependencies,
): Promise<void> {
const currentOrder = await dependencies.orders.find(command.orderId);
const decision = decideOrderChange(currentOrder, command);
if (decision.kind === "ignore-stale-event") return;
if (decision.kind === "no-change") return;
const enterpriseOrder = mapToEnterpriseOrder(decision.nextOrder);
await dependencies.transaction.run(async (unitOfWork) => {
await unitOfWork.orders.save(decision.nextOrder);
await unitOfWork.outbox.add({
deliveryId: `${command.webhookId}:internal-order-api`,
destination: "internal-order-api",
message: enterpriseOrder,
});
await unitOfWork.outbox.add({
deliveryId: `${command.webhookId}:event-hubs`,
destination: "enterprise-order-events",
message: {
eventId: command.webhookId,
eventType: "OrderChanged",
partitionKey: command.shopId,
correlationId: command.correlationId,
body: enterpriseOrder,
},
});
});
}
Separate outbox dispatchers deliver the internal API notification and the enterprise event. Each destination has its own retry and idempotency policy. This avoids pretending that a database commit, an HTTP call, and Event Hubs publication form one atomic distributed transaction.
The Azure adapter uses EventHubProducerClient with DefaultAzureCredential, allowing the deployed workload to use managed identity instead of storing an Event Hubs connection string:
const producer = new EventHubProducerClient(
`${eventHubsNamespace}.servicebus.windows.net`,
eventHubName,
new DefaultAzureCredential(),
);
Copilot generates the repetitive adapter code, but the instructions and skills constrain where that code is allowed to live.
Pattern 7: MCP, validation, and independent review close the loop
MCP servers provide external context and tools. The GitHub MCP integration can connect the implementation to the issue, pull request, review activity, and repository history. Additional organization-approved MCP servers can expose documentation, test systems, or observability data.
I keep production access narrow. A code-generation agent does not automatically receive permission to change a production database or deployment.
After implementation, I ask Copilot to run the repository’s validation workflow:
Use the integration-validation skill.
Run formatting, linting, TypeScript type checking, unit tests, webhook
contract tests, duplicate and out-of-order delivery tests, Service Bus
integration tests, Event Hubs adapter tests, infrastructure validation,
and a production build. Do not claim a check passed unless it ran.
The test matrix includes more than the happy path:
| Scenario | Expected behavior |
|---|---|
| Invalid Shopify signature | Reject before parsing or enqueueing |
| Duplicate webhook ID | Return success without repeating side effects |
| Older update arrives late | Preserve newer order state |
| Service Bus temporarily unavailable | Fail acknowledgement so Shopify can retry, without recording acceptance prematurely |
| Internal API timeout | Its durable outbox delivery retries with a stable idempotency key |
| Database commit succeeds, Event Hubs is unavailable | The Event Hubs outbox delivery remains pending |
| Event is delivered twice downstream | Stable event ID allows consumer deduplication |
| Shop is uninstalled or token revoked | Stop privileged calls and move installation to an explicit state |
| Private Storefront token appears in client bundle or logs | Fail security review |
Finally, I switch to a read-only code-review agent:
Review the completed diff without editing it.
Check Shopify authentication, token boundaries, API scopes, webhook
idempotency and ordering, Service Bus acknowledgement semantics,
database consistency, outbox publication, Event Hubs partitioning,
managed identity, telemetry redaction, failure-path tests, deployment
impact, and rollback safety. Report only evidence-backed findings with
file and line references.
I inspect /diff, resolve findings, and rerun the affected validations. For long-running work I can resume the session; for suitable repository work I can delegate to the cloud agent, but the pull request and required checks remain the control point.
The pattern underneath my patterns
My Copilot CLI workflow now looks like this:
Instructions define permanent standards.
Skills define repeatable procedures.
Specialist agents investigate focused concerns.
Plan mode defines boundaries and dependencies.
Fleet parallelizes independent implementation.
Generated code stays behind explicit adapters.
MCP supplies approved external context.
Validation and independent review protect the result.
The real value is not that Copilot can generate a webhook handler or an Event Hubs publisher.
The value is that I can use one terminal workflow to reason about the feature, divide the work, preserve project knowledge, generate code, validate failure paths, and review the final diff.
I am still responsible for the architecture, credentials, permissions, production behavior, and final decision.
Copilot CLI helps me execute that responsibility with more structure and less context switching.