Skip to content

Sourcing agent — design

  • Status: draft for review (2026-11-03)
  • Scope: an autonomous Copilot Studio agent that works as a Sourcing Manager on ProjectIQ purchasing data, from the approved purchase request to the supplier invoice.

1. Goal and decisions

The buyer stops opening RFQs and invoices "just to check": the agent prepares every sourcing decision with numbers and a reason, does the routine supplier follow-ups on its own, and keeps a log of what it did.

# Decision
D1 Ten tasks, same priority, delivered in pipeline order (section 2)
D2 Platform: Copilot Studio, autonomous agent with Dataverse event triggers and recurrences
D3 Writes only through proposals: the agent prepares, a person approves, then the existing Custom API runs
D4 Proposals are rows of a new table op365_agentproposal, decided from a Teams adaptive card or from the app
D5 Supplier messages go through the existing SupplierMailer (queue op365_SupplierEmailSender, templates by supplier language)
D6 Purchasing proposals are decided by the new role OP365 Buyer; invoice proposals by the Purchasing Approver (segregation of duties); the project manager is informed
D7 Approach "hybrid": every number comes from deterministic, unit-tested Custom APIs in C#; the LLM chooses what to do and explains why
D8 Everything (table, Custom APIs, roles, environment variables, the agent and its flows) lives in the single solution OpenProject365; the agent is off by default

A new ADR docs/adr/0003-sourcing-agent-proposals.md records D3, D4 and the trust rule of section 5. ADR 0001 (Copilot and Cowork read-only) stays valid for Copilot and Cowork.

2. Tasks

🟢 = the agent acts on its own (reads, analyses, notifications) · 🟡 = the agent creates a proposal, a person decides.

# Task Mode Analysis Custom API Outcome
1 Route an approved purchase request: RFQ or direct order 🟡 op365_SourcingRouteRequest Proposal Source via RFQ or Direct Order
2 Choose the suppliers to invite (rating, valid documents, lead time, price history) 🟢 inside 1 op365_SourcingSuggestSuppliers Invitation list in the payload of task 1
3 Analyse the quotes (price, delivery, risk) and recommend the award 🟡 op365_SourcingCompareQuotes Proposal Award RFQ
4 Remind suppliers who have not answered an RFQ 🟢 op365_SourcingLateItems E-mail RFQ Reminder + Log
5 Supplier compliance: expiring or expired documents of suppliers with open RFQs or orders 🟢 op365_SourcingLateItems Insight to the buyer; non-compliant suppliers excluded by task 2
6 Three-way match invoice / order / goods receipt 🟡 op365_SourcingMatchInvoice Proposal Approve Invoice or Reject Invoice
7 Late orders: confirmed, not received beyond the lead time, impact on the critical path 🟢 op365_SourcingLateItems E-mail Order Delay + Insight to the PM when the WBS node is critical
8 Project budget: remaining WBS budget (budget − actual − committed) 🟢 op365_SourcingBudgetCheck Insight Budget Alert to the PM
9 Spend analysis: fragmented purchases of the same item or supplier across projects 🟢 op365_SourcingSpendInsights Insight Consolidation
10 Supplier scouting and onboarding: categories with one supplier, new suppliers not on the portal 🟡 op365_SourcingCategoryCoverage Insight New Supplier Needed, proposal Invite Supplier
flowchart LR
    RdA[Approved request] -->|1| D{RFQ or direct?}
    D -->|2| INV[Invitations] -->|4| Q[Quotes] -->|3| AW[Award proposal]
    AW --> PO[Order] -->|7| GR[Goods receipt] -->|6| INVC[Invoice]
    C5[5 Compliance] -.-> INV
    B8[8 Budget] -.-> RdA
    S9[9 Spend] -.-> D
    N10[10 New suppliers] -.-> INV

3. Architecture

flowchart TB
    subgraph SOL["Solution OpenProject365"]
      direction TB
      TRG["Triggers: Dataverse events + daily/weekly recurrence"] --> CS["Copilot Studio agent<br/>Sourcing Manager"]
      CS -->|tool| API["Analysis Custom APIs op365_Sourcing* (read-only, JSON)"]
      CS -->|tool| NOT["op365_NotifySupplier (SupplierMailer)"]
      CS -->|create| PROP[("op365_agentproposal")]
      PROP --> FL["Flow: Teams card"] -->|DecidedBy| DEC["op365_DecideAgentProposal"]
      APP["App: Approve / Reject commands"] --> DEC
      DEC -->|impersonating the decider| EX["Existing Custom APIs<br/>CreateRfq · SendRfq · AwardRfq · CreatePurchaseOrder<br/>DecideSupplierInvoice · InviteToPortal"]
    end
Topic Decision
Agent identity Application user ProjectIQ Sourcing Agent with role OP365 Sourcing Agent: reads, creates proposals, notifies suppliers; never writes business tables, never decides
No drafts The agent does not create draft RFQs or orders. On approval DecideAgentProposal runs the whole chain (e.g. CreateRfq → apply the invitation list of the payload → SendRfq), so a rejected proposal leaves nothing behind
Execution DecideAgentProposal checks the decider, then calls the target Custom API with IOrganizationServiceFactory.CreateOrganizationService(deciderId): status rules, EnsureManager, IsSecondLevelApprover and auditing apply to the person who approved
Idempotency One open proposal per DedupKey (table:id:action), enforced by plugin
Calculation errors An analysis Custom API that fails produces a Log row with the error and no proposal; the agent never falls back to computing numbers itself

4. Data model

Table op365_agentproposal (Agent Proposal)

User-owned, notes and auditing on. Parental relationship from Project (cascade share), so project access-team members see the proposals of their projects.

Column Type Notes
Name Text Generated, e.g. "Award RFQ-000123 → Rossi Srl"
Kind Choice Proposal, Insight, Log
ActionType Choice See below
ProjectId, PurchaseRequestId, RfqId, InvitationId, PurchaseOrderId, SupplierInvoiceId, SupplierId Lookups Typed lookups instead of a polymorphic regarding: filtered views and subgrids on the existing forms
Rationale Multiline text Explanation written by the agent
Evidence Multiline text (JSON) Output of the analysis Custom API; column-secured (OP365 Cost Visibility)
Payload Multiline text (JSON) Inputs of the action (e.g. suppliers to invite, winning invitation, rejection note)
Amount, EstimatedSaving Currency Sorting and dashboard
DedupKey Text table:id:action
ExpiresOn Date CreatedOn + op365_SourcingProposalExpiryDays
DecidedById, DecidedOn, DecisionNote Lookup, date, text Set by DecideAgentProposal
ResultId, Error Text Record created by the action, or the error message

Rationale, Evidence, Payload, Kind, ActionType and the lookups cannot change after create; the status changes only through the Custom APIs.

Action types (choice values from 365000000)

ActionType Kind Tasks Runs on approval Decided by
Source via RFQ Proposal 1-2 CreateRfq, invitation list of the payload, SendRfq Buyer, PM, admin
Direct Order Proposal 1 CreatePurchaseOrder Buyer, PM, admin
Award RFQ Proposal 3 AwardRfq(InvitationId) Buyer, PM, admin
Approve Invoice / Reject Invoice Proposal 6 DecideSupplierInvoice Purchasing Approver, admin
Invite Supplier Proposal 10 InviteToPortal Buyer, admin
Supplier Reminder Log 4, 7 – (e-mail already sent) –
Compliance Alert, Budget Alert, Late Delivery, Consolidation, New Supplier Needed Insight 5, 7, 8, 9, 10 – –

Life cycle

stateDiagram-v2
    [*] --> Pending: Proposal
    [*] --> Open: Insight
    [*] --> Done: Log
    Pending --> Executed: Approve, action succeeded
    Pending --> Failed: Approve, action failed
    Pending --> Rejected: Reject (note required)
    Pending --> Expired: past ExpiresOn
    Pending --> Superseded: target no longer valid
    Failed --> Pending: Retry
    Open --> Acknowledged

Active: Pending, Open, Failed. Inactive: Executed, Rejected, Expired, Superseded, Acknowledged, Done. Rejection notes are the feedback used to tune the agent instructions.

Environment variables (all in OpenProject365)

Schema name Type Default Read by
op365_SourcingAgentEnabled Yes/No no Agent triggers (stop at once when no); open proposals stay decidable
op365_SourcingAgentUser Text (application user name) – DecideAgentProposal: the only caller trusted to pass DecidedBy
op365_SourcingApprovalTeam Text (Teams team and channel) – Teams card flow
op365_SourcingDirectOrderLimit Number – SourcingRouteRequest: below it, propose a direct order
op365_SourcingMinQuotes Number 3 SourcingSuggestSuppliers
op365_SourcingReminderDays Number 2 SourcingLateItems: days before the RFQ deadline
op365_SourcingBudgetWarningPct Number 90 SourcingBudgetCheck
op365_SourcingProposalExpiryDays Number 5 Proposal create, ExpireAgentProposals

New Custom APIs

Custom API Binding Does
op365_SourcingRouteRequest Purchase request Recommended route, suppliers to invite, budget check (JSON)
op365_SourcingSuggestSuppliers Purchase request Ranked suppliers with reasons; excludes blocked and non-compliant suppliers
op365_SourcingCompareQuotes RFQ Same rule as the QuoteComparison control: best price per line, best total, delivery, coverage
op365_SourcingMatchInvoice Supplier invoice Price and quantity differences against order lines and goods receipts, reusing InvoiceLogic
op365_SourcingLateItems Unbound RFQ invitations to remind, late orders (with critical-path flag), compliance issues of active suppliers
op365_SourcingBudgetCheck Project or WBS node Budget, actual, committed, remaining, % used per WBS node
op365_SourcingSpendInsights Unbound Fragmented purchases by item, supplier and category over a period
op365_SourcingCategoryCoverage Unbound Categories with fewer active, compliant suppliers than op365_SourcingMinQuotes
op365_NotifySupplier Account Sends a supplier template (RFQ Reminder, Order Delay, Portal Invitation) regarding a record; output Sent
op365_DecideAgentProposal Agent proposal Inputs Approve, Note, DecidedBy (optional); outputs Status, ResultId
op365_RetryAgentProposal Agent proposal Failed → Pending
op365_ExpireAgentProposals Unbound Pending past ExpiresOn → Expired; output Count

Analysis Custom APIs return spend and quote prices only when the caller has the OP365 Cost Visibility profile; they never return IBAN or hourly costs.

5. Triggers and flows

Trigger Tasks
Event: purchase request → Approved 1, 2, 8
Event: RFQ invitation → Responded or Declined, when every invitation has answered 3
Event: supplier invoice → To Review (reading completed) 6
Event: new supplier account not on the portal 10
Daily 07:00 (after Daily document expiry at 06:00) 3 (sent RFQs past the deadline), 4, 5, 7, 8
Weekly, Monday 9, 10
Daily flow (deterministic, no agent) op365_ExpireAgentProposals
sequenceDiagram
    participant AG as Agent (application user)
    participant DV as Dataverse
    participant FL as Card flow
    participant B as Buyer / Purchasing Approver (Teams)
    participant PM as Project manager
    AG->>DV: analysis Custom API → Evidence
    AG->>DV: create Agent Proposal (Pending, Rationale, Payload)
    DV-->>FL: row added, Kind = Proposal
    FL->>B: adaptive card in op365_SourcingApprovalTeam
    FL->>PM: in-app notification (FYI)
    B->>FL: Approve / Reject + note
    FL->>DV: DecideAgentProposal(Approve, Note, DecidedBy)
    DV->>DV: caller = op365_SourcingAgentUser? decider allowed?
    DV->>DV: target still valid? else Superseded
    DV->>DV: run target Custom API impersonating the decider
    DV-->>FL: Executed + ResultId, or Failed + Error
    FL->>B: update the card (outcome and link)

Deciding from the app calls DecideAgentProposal directly with the person as caller; a later click on the Teams card gets "This proposal has already been decided."

6. Security

Role Privileges
OP365 Buyer (new) Organization Read on Project and its child tables; Organization full access on purchase requests, RFQs, invitations, quotes, orders, goods receipts, supplier accounts and contacts; supplier invoices Read and Append To; Agent Proposal Organization Read and Write. Additive to Basic User; assign OP365 Cost Visibility
OP365 Sourcing Agent (new, application user only) Organization Read on master data, project and purchasing tables; Create on Agent Proposal; OP365 Cost Visibility (needed for spend analysis)
OP365 User Agent Proposal at User level: sees the proposals of their projects through the access team (parental relationship)
OP365 Administrator Full access

Code changes:

Where Change
Purchasing.EnsureManager Admin, project manager or OP365 Buyer (Rules.IsBuyer)
DecideSupplierInvoice None: invoices stay with the Purchasing Approver or an administrator
DecideAgentProposal DecidedBy accepted only when the caller is op365_SourcingAgentUser; the decider must be allowed for the action type; then impersonation
Agent Proposal Not exposed on the supplier portal (no table permission)

7. ALM and tests

Component Source of truth How it reaches Dev
Pure rules (routing, supplier score, quote comparison, three-way match, late items, budget, consolidation) src/plugins/OpenProject365.Plugins/SourcingLogic.cs Plugin build
Analysis Custom APIs SourcingAnalysis.cs plugins.py / deploy-dev
Proposal APIs and plugins, NotifySupplier Sourcing.cs plugins.py / deploy-dev
Table, choices, environment variables, roles, Custom API definitions, form, views, commands tools/dataverse/sourcing_agent.py (idempotent) Script, then export
Supplier templates tools/dataverse/supplier_email_templates.py Script
Card and expiry flows tools/dataverse/flows.py, flow_definitions.py Script
Agent (instructions, triggers, tools) Copilot Studio components of the solution Maker portal in Dev, then export-dev and pull request
Translations tools/dataverse/translations.json translations.py

Deployment: connection references (Dataverse, Teams) and the new variable values in deployment/*.settings.json. Manual steps, documented in Administration: create the application user and assign its roles, assign OP365 Buyer, publish the agent and bind its triggers to the application user's connection. In the AppSource package the agent ships disabled.

Level What Where
Unit Every rule of SourcingLogic.cs; DecideAgentProposal cases: untrusted caller, decider without role, already decided, superseded target, reject without note OpenProject365.Plugins.Tests, CI
Smoke tools/dataverse/sourcing_smoke.py: seed data → analysis APIs → manual proposal → Approve → RFQ / order created Dev
Agent Copilot Studio evaluation set: at least 3 scenarios per task (normal, edge, missing data); checks the action type and that the rationale quotes the evidence Dev, before deploy-test
Acceptance One full cycle request → payment with a real buyer Test

Delivery order, one pull request each, manuals included:

flowchart LR
    F0["0 Foundations: table, roles, EnsureManager,<br/>DecideAgentProposal, Teams card, ADR 0003"] --> F1[1-2] --> F2[3] --> F3[4-5] --> F4[6] --> F5[7-8] --> F6[9-10]

8. Documentation

File Change
docs/{en,it,fr,de}/sourcing-agent.md (new) What the agent does, who does what, proposals and life cycle, deciding from Teams and from the app, insights and log, what suppliers receive, views, messages
docs/*/purchasing.md OP365 Buyer in "Who does what"; links to the agent from RFQs and invoices
docs/*/administration.md Roles, the new company settings, first-setup checklist, licences (Copilot Studio), background automations
docs/*/supplier-portal.md The new reminder, delay and invitation e-mails
docs/*/copilot.md Copilot/Cowork (read-only) versus the agent (writes through proposals)
README.md, mkdocs.yml Row in "User manuals", menu entry in four languages
docs/data-model.md, docs/dev-environment.md Table, Custom APIs, security; agent ALM
docs/adr/0003-sourcing-agent-proposals.md The decision record

python3 tools/docs/check_docs.py must report 0 errors in every pull request.

Out of scope

Autonomous execution without approval, thresholds for autonomy, free-text e-mails written by the LLM, negotiation with suppliers, external supplier databases for scouting, exposure of proposals on the supplier portal.