Scenario: from an idea to a delivery backlog

A business user types an idea into a Copilot Studio agent. The product squad researches it, plans it, produces the stakeholder deliverables, and hands back a structured backlog — which the agent then creates as epics, stories, and tasks in Azure DevOps, Jira, or GitHub. This page is the whole path, end to end, so you can reproduce it.

Two things this scenario is not. The server never writes to Azure DevOps, Jira, or GitHub — the agent does, through the certified native connector on the user's own connection. And the squad's output is advisory: finished text and a validated plan, not executed code.

What you will have at the end

  1. A deployed MCP server in your own Azure tenant, with the product profile and the business tools enabled.
  2. A Copilot Studio agent with two connectors: this server, and your native work-tracking connector.
  3. A repeatable conversation: "turn this PRD into a backlog" → confirmation → work items created, correctly parented, with acceptance criteria intact.
  4. A durable trail under .copilot-tracking/ in your own storage, read back automatically on the next run.

Part 0 — Prerequisites

Part 1 — Deploy with the product path switched on

Follow host/RUNBOOK.md steps 1–7 for the base deployment. This scenario needs more than the default advisory posture, so set the following before Step 6 (they are main.bicepparam parameters as well as environment variables):

SettingValueWhy this scenario needs it
SQUAD_MCP_REMOTE_PIPELINE_ENABLEDtrueServes squad_run and squad_status. Off by default.
SQUAD_MCP_RUN_STATE_BACKENDtableA product run is long. Table state survives scale-to-zero and makes approval visible across replicas.
SQUAD_MCP_STORAGE_ACCOUNTyour accountBacks run state, memory, and overflow.
SQUAD_MCP_WORKER_ENABLEDtrueA 10-stage product run exceeds the request path. The worker drives it off-request.
SQUAD_MCP_ENABLE_BUSINESS_TOOLStrueServes squad_backlog — the JSON contract this whole scenario turns on.
SQUAD_MCP_ENABLE_MEMORYtruePersists the .copilot-tracking/ tree.
SQUAD_MCP_MEMORY_BACKENDtable or graphWhere that tree lands. Decide this in Part 2 before you deploy.
SQUAD_MCP_MEMORY_AUTO_ENABLEDtrueReads prior state back into context automatically, so the backlog call already knows what the run decided.
SQUAD_MCP_ENABLE_ARTIFACTStrueWrites the ledger — team.md, routing.md, state.json — so the squad is inspectable.
SQUAD_MCP_ADVISORY_AUTOPILOT_ENABLEDtrueOptional but recommended here. See the note below.
SQUAD_MCP_RUN_ENCRYPTION_KEY_B64a base64 keyEncrypts stage artifacts and caller input at rest. Requirements documents are usually confidential.

About autopilot. squad_run normally holds at a Human Gate and needs an operator to release it — which is fine for you, and confusing for a business user who just wants a backlog. The product profile carries no impactful role (no deployer, no IaC author, no backlog executor), so it resolves as advisory-only, and SQUAD_MCP_ADVISORY_AUTOPILOT_ENABLED=true lets it run to completion unattended.

This narrows the gate; it does not remove it. Ask for anything that routes an impactful role and the run still holds. If you would rather keep every run gated, leave it off and use the operator release in Part 6.

Part 2 — Decide where the history is saved

This is the decision people skip, and then discover after their first real run that a month of squad output lives somewhere nobody can open. Decide it now — it is one setting, and it is the same setting for everything the squad writes.

What actually gets written

With SQUAD_MCP_ENABLE_ARTIFACTS=true a run writes a browsable .copilot-tracking/ tree — the same tree the squad produces when it runs locally in VS Code:

squad/team.md                          the 13 seeded roles
squad/routing.md                       the rows those roles can serve
squad/state.json                       the ledger
squad/decisions.md                     append-only — incl. the Intake Readiness Verdict
squad/notifications.md                 append-only
squad/history/<agent>.md               per agent, with a measured Consumption block
squad/history/autopilot-run-<id>.md    per run
squad/consumption.md                   rebuilt from those blocks, so earlier turns are never dropped
plans/ · docs/ · outputs/ · ppt/<date>/<slug>/   the deliverables themselves

Without SQUAD_MCP_ENABLE_ARTIFACTS, auto-memory keeps three flat keys. That is enough for continuity between two turns and nothing you can audit — you cannot open the PRD a run produced or see which agent wrote what. For this scenario, turn it on.

Pick a destination

One setting, SQUAD_MCP_MEMORY_BACKEND, chooses it for memory, the ledger, and the deliverables at once.

BackendWhere it landsChoose it whenWhat you must do
table
default
Azure Table Storage in your account, table squadmemory You want it durable, cheap, and multi-replica safe, and you read it through the tools rather than by hand. Set SQUAD_MCP_STORAGE_ACCOUNT. Nothing elsemain.bicep assigns Storage Table Data Contributor to the app and worker identity for you.
graph A SharePoint document library or OneDrive, one readable .md file per entry Business users need to open the output themselves. For a product-profile scenario this is usually the right answer. Set SQUAD_MCP_MEMORY_GRAPH_DRIVE_ID and run a separate, admin-privileged deployment — see below.
file A local directory in the container A single-replica trial. Not durable across a scale-to-zero restart. Set SQUAD_MCP_MEMORY_DIR. Do not combine with the worker.

Run state and memory are two different stores. SQUAD_MCP_RUN_STATE_BACKEND holds the in-flight run so it survives a restart; SQUAD_MCP_MEMORY_BACKEND holds the history you keep. They can differ — table run state with graph memory is a normal combination — and they both read SQUAD_MCP_STORAGE_ACCOUNT, which is why the two are easy to confuse.

If you chose SharePoint

Each entry becomes one markdown file at:

<rootPath>/<tenantId>/<project>/<path>.md

versioned by SharePoint and subject to your existing retention, search, and DLP policy. Concurrency uses Graph's native eTag with If-Match, so a stale write loses the race rather than clobbering.

Two steps you must perform yourself:

  1. Resolve the drive id of the library that will hold the history:
    SITE_ID=$(az rest --method GET \
      --url "https://graph.microsoft.com/v1.0/sites/<TENANT>.sharepoint.com:/sites/<SITE_PATH>" \
      --query id --output tsv)
    
    az rest --method GET \
      --url "https://graph.microsoft.com/v1.0/sites/$SITE_ID/drives" \
      --query "value[].{name:name,id:id}" --output table
  2. Grant the app identity access to that one library, as an administrator:
    az deployment group create \
      --resource-group "$RG" \
      --template-file host/infra/graph-memory-permissions.bicep \
      --parameters host/infra/graph-memory-permissions.bicepparam
    This assigns Sites.Selected — which on its own grants access to no site — and then grants write on exactly the site you named. It is a separate deployment on purpose: it needs AppRoleAssignment.ReadWrite.All and Sites.FullControl.All, far above what deploying the Container App needs, so your routine deploys never require Graph admin rights. Re-running it is a no-op.

Leave SQUAD_MCP_MEMORY_GRAPH_ENCRYPT off unless policy forces it. The entire reason to target SharePoint is that a human can open the file; encrypting the content defeats that. If you do enable it, configure runEncryptionKeyBase64 as well.

Leaving sharePointSiteId empty assigns Sites.Selected only. That is a deliberate safe partial state — the identity is eligible but reaches nothing, so a half-finished onboarding never silently exposes a library.

Large artifacts

A table entity has a size ceiling, and a PRD or a full backlog will exceed it. Enable overflow so the write does not fail:

VariableValue
SQUAD_MCP_MEMORY_OVERFLOW_ENABLEDtrue
SQUAD_MCP_MEMORY_OVERFLOW_CONTAINERa blob container name
SQUAD_MCP_MEMORY_OVERFLOW_THRESHOLD_BYTES32768 default

Anything past the threshold spills to blob with a pointer left in the entity. Reads are transparent.

Offering a choice to different teams

If one deployment serves several teams that must not share a destination, declare an allow-list instead of a single backend:

SQUAD_MCP_MEMORY_TARGETS=[
  { "name": "azure",      "backend": "table", "tableName": "squadmemory" },
  { "name": "sharepoint", "backend": "graph", "driveId": "<DRIVE_ID>", "rootPath": "squad-memory" }
]
SQUAD_MCP_MEMORY_DEFAULT_TARGET=azure

The caller then passes an opaque target name. You own every credential-bearing field; an undeclared name is rejected before any I/O and never falls back to the default.

Reading it back

There are two paths, and for this scenario the first one is the one that matters:

Turn the memory tools off in the agent. With automatic memory on, the generated instructions' memory section must be deleted. Left in, the agent calls the tools too, invents a different project name each session, and continuity silently disappears — which looks exactly like the feature not working.

What is true regardless of destination

Part 3 — Expose the scopes this scenario uses

In the Entra app registration from Step 2 of the runbook, expose and consent to:

ScopeGranted toFor
Squad.Runthe Copilot Studio connectionStarting the product run and polling it with squad_status.
Squad.Backlogthe Copilot Studio connectionsquad_backlog. Separate from Squad.Business so it is independently revocable.
Squad.Memorythe Copilot Studio connectionOptional — only if you want the agent to browse history with squad_history.
Squad.Operateyou, as an app roleReleasing a held run. Never grant this to the agent.

Every scope is fail-closed: a missing one returns 403 with no work performed and nothing billed.

Part 4 — Import the connector into Copilot Studio

  1. Regenerate the connector so it reflects your build, then fill in the placeholders in generated/copilot-studio-connector/apiDefinition.swagger.json and apiProperties.json:
    npm run generate:connector
    PlaceholderReplace with
    <SQUAD_MCP_HOST>your Container App FQDN, host only, no scheme
    <ENTRA_TENANT_ID>your tenant id
    <ENTRA_CLIENT_ID>the app id from Step 2
    <SQUAD_MCP_AUDIENCE>api://<ENTRA_CLIENT_ID>
  2. Add a custom connector in Copilot Studio from the OpenAPI file. It advertises the x-ms-agentic-protocol: mcp-streamable-1.0 /mcp operation.
  3. Complete the Entra OAuth 2.0 connection, consenting to the scopes in Part 3.
  4. Enable generative orchestration on the agent. Without it the agent cannot call MCP tools at all — this is the single most common reason "nothing happens".
  5. Add your work-tracking connector to the same agent: the certified Azure DevOps, Jira, or GitHub connector, authenticated as the end user.
  6. Paste the generated agent instructions from generated/copilot-studio-connector/agent-instructions.md into the agent's Instructions field. It already contains the create-work-items protocol, the confirmation requirement, and the parenting rules. Delete the memory section — you enabled automatic memory in Part 1, so the agent must not call the memory tools.

Part 5 — What the product profile actually runs

profile: "product" seeds 13 roles, and that seeding is what makes this scenario different from a generic squad_run. Resolved against the bundled cast, a product run sequences:

#StageRoleResolved agent
0Intake gateintake-validatorPRD Quality Reviewer
1ResearchresearcherSquad Researcher
2PlanleadSquad Lead
CouncilInterleaved here only when the request engages it. A backlog request usually does not.
3Deliverable fan-outanalystPRD Builder
4product-ownerFunctional Planner
5designerUX UI Designer
6experimenterExperiment Designer
7presenterPowerPoint Subagent
8technical-writerSquad Technical Writer
9data-scientistDS Gen Data Spec
10ReviewtesterSquad Reviewer

Two behaviours here are worth understanding before you run it:

Each specialist writes to its own root, which is where you go looking afterwards:

RoleDeliverable root
analyst, product-owner, designer, experimenter.copilot-tracking/plans
presenter.copilot-tracking/ppt/<date>/deck
technical-writerdocs
data-scientistoutputs

Part 6 — Run it

In the Copilot Studio agent, the user says something like:

Run the full product squad on our customer onboarding PRD and turn it into a delivery backlog.

The agent calls squad_run. Make sure your instructions pass the profile:

{
  "name": "squad_run",
  "arguments": {
    "request": "Turn our customer onboarding PRD into a delivery backlog",
    "profile": "product",
    "mode": "autopilot",
    "squad": "onboarding",
    "context": "<the PRD text, or the constraints the user gave>"
  }
}

squad is the project partition. Use a stable name per initiative — it scopes the memory, the ledger, and the history that the next run reads back. default works, but everything then shares one tree.

What comes back depends on your Part 1 choice:

AutopilotBehaviour
enabled The run proceeds unattended. The agent gets a run id; the user polls with squad_status until the compiled artifact returns.
disabled The run holds. The agent reports the run id and says it awaits approval — it must not claim the work is done. You release it out of band:
curl -sS -X POST "https://$FQDN/admin/approve" \
  -H "Authorization: Bearer $OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"runId":"<run-id>"}'
Then a squad_status poll drives it to completion. The release is tenant-scoped and audited with approver and timestamp.

Part 7 — Turn the result into work items

The compiled artifact is prose — good for a human, wrong for a connector. squad_backlog is the converter:

{
  "name": "squad_backlog",
  "arguments": {
    "request": "Break the approved onboarding plan into epics, stories, and tasks",
    "squad": "onboarding"
  }
}

With automatic memory on you do not have to paste the run's output back in — the pre-read merges the project's prior state into context as data, so passing the same squad is enough. Pass context explicitly only to add something the run never saw, such as an existing backlog or a definition of done.

The contract you get back

{
  "summary": "Plain-language summary for the business user.",
  "epics": [ { "title": "...", "description": "...",
               "acceptanceCriteria": ["..."],
               "stories": [ { "title": "...", "tasks": [ ... ] } ] } ],
  "workItems": [
    { "ref": "E1",       "type": "Epic",       "title": "...", "acceptanceCriteria": ["..."] },
    { "ref": "E1-S1",    "type": "User Story", "parentRef": "E1",    "estimate": "M", "...": "..." },
    { "ref": "E1-S1-T1", "type": "Task",       "parentRef": "E1-S1", "...": "..." }
  ]
}

workItems is a depth-first flattening of epics, so parents always precede their children. Iterating it in order and creating as you go means a parent id always exists by the time a child needs it. ref and parentRef are assigned by the server, not the model — which is the point: the agent links by id it recorded, never by matching titles, because titles are not unique and a model will rephrase them.

The server validates and normalizes the model's JSON and fails cleanly if it cannot, so the agent never receives half-parsed output. Hard caps apply — 30 epics, 30 stories per epic, 30 tasks per story, 20 acceptance criteria, 4000 characters per text field — so one call can never emit an unbounded backlog.

The agent's create loop

This is already in the generated instructions; it is repeated here so you can verify the behaviour you see:

  1. Show the user the summary and the epic/story list. Ask for confirmation. Never bulk-create unconfirmed.
  2. On confirmation, iterate workItems in order, one connector call per element: type from type, title from title, description from description, and the acceptanceCriteria lines joined as a list.
  3. Record the created id against the element's ref. When an element has a parentRef, link it to the id recorded for that ref.
  4. On a failure, report which ref failed, continue with the rest, then offer to retry only the failures.
  5. Pace the calls. The connectors are rate limited per connection — create one epic and its stories at a time rather than firing the whole array at once.

Part 8 — Mapping to your tracker

The contract uses the Azure DevOps type names. That is a naming choice, not a lock-in — the mapping happens in your agent's instructions, and the server is unaware of which tracker you use.

Contract typeAzure DevOpsJiraGitHub
EpicEpicEpicIssue labelled epic
User StoryUser Story (Product Backlog Item on Scrum)StoryIssue labelled story, as a sub-issue of the epic
TaskTaskSub-taskSub-issue, or a task-list item on the story
parentingAdd link, parent/childissue link / parent fieldsub-issues

Check your ADO process template. User Story exists in Agile; Scrum calls it Product Backlog Item and CMMI calls it Requirement. If creates fail with an unknown work item type, add the substitution to your agent instructions — the server will keep emitting User Story.

GitHub has no native epic. The three-level hierarchy maps onto issues plus sub-issues, so decide the convention up front and put it in the instructions. Otherwise the agent will invent one, differently each time.

Part 9 — Verify it worked

CheckExpected
The run's ledgerteam.md lists the 13 product roles; routing.md shows only rows those roles can serve.
The decision logAn ## Intake Readiness Verdict block, if the run was grounded in a document.
Deliverable rootsArtifacts under .copilot-tracking/plans, docs, and outputs.
The trackerEvery ref created once, children parented, acceptance criteria present on stories.
The next runAsk a follow-up with the same squad. It should already know what was decided.

Troubleshooting

SymptomCauseFix
The agent answers from its own knowledge and never calls a toolGenerative orchestration is off, or the instructions were not pastedEnable it, and paste the generated instructions block
squad_run is not in the tool listThe pipeline is disabledSQUAD_MCP_REMOTE_PIPELINE_ENABLED=true plus a durable run-state backend
squad_backlog is not in the tool listBusiness tools are offSQUAD_MCP_ENABLE_BUSINESS_TOOLS=true, and a model endpoint must be configured
403 on a tool callThe scope was never consentedGrant the scope in Part 2 and re-authorize the connection
401 on every callToken audience or issuer not acceptedCheck <SQUAD_MCP_AUDIENCE> matches api://<client-id>
403 origin_not_allowedOrigin allow-listAdd the caller origin to SQUAD_MCP_ALLOWED_ORIGINS. * is rejected at startup
The run never finishesIt is held at the gate, or exceeds the request pathApprove it, or enable autopilot; and enable the worker for long runs
The run stops early with intake_not_readyThe intake gate rejected the inputsRead the verdict, fix the requirements document, re-run. This is intended behaviour
The squad has the wrong rolesThe project already had a squadThe seeded roster wins over the profile hint. Use a new squad name
Only one giant work item was createdThe agent parsed prose instead of calling squad_backlogConfirm the tool is reachable and the instructions route backlog requests to it
Children are attached to the wrong parentThe agent matched on titleIt must match on ref. Re-paste the generated instructions
Creates fail partway with throttlingThe whole array was fired at onceBatch: one epic and its stories per pass
The next run remembers nothingThe agent still calls the memory tools and invents a new project name each sessionDelete the memory section from the agent instructions, and pass a stable squad
A large artifact fails to saveTable entity size ceilingEnable SQUAD_MCP_MEMORY_OVERFLOW_ENABLED with a blob container
Nothing appears in the SharePoint librarySites.Selected was assigned but no site was grantedRe-run graph-memory-permissions.bicep with sharePointSiteId filled in
History vanishes after a restartThe file memory backend on a scale-to-zero appMove to table or graph

What this scenario deliberately does not do