How to Say No to a Feature Request
Minute thirty-eight of a sixty-minute technical call. The commercial part went well. Two of their engineers have been quiet for twenty minutes, and then one of them unmutes and says the thing:
"So we'll need to run a small script at login. It's not much — maybe forty lines. Where do we upload it?"
Everyone turns, in the way people turn on video calls, which is to say a small silence happens and then your name appears in the transcript. You are the engineer. The account executive is watching. The customer's architect is not being unreasonable; he has done this before, at his last company, on a product that let him. He has already told his VP it's a two-day integration.
And you know — immediately, before you can articulate why — that the answer is no.
The next ninety seconds decide whether this is a productive call or a lost one. Most engineers spend them explaining their own architecture, which is the wrong thing to spend them on.
This is not an article about negotiation; there's no technique here for getting people to want what you have. It's about requirements, and about a discipline most of us know abstractly and abandon in the moment: the request that arrives on the call is not a requirement. It is a proposed implementation. Your job is to recover the requirement underneath it, because the implementation is almost always negotiable and the requirement almost never is.
Most bad architecture enters a product because someone shipped the proposal instead of the requirement.
The request is already a design
Nobody sends you a requirement. By the time a request reaches a vendor call it has been through three or four rounds of internal design at the customer, and it arrives already committed to a mechanism.
"Let us run a script at login" is not a need. It's an answer to a need, arrived at by an architect reasoning about a system he doesn't have access to, using a model of identity platforms formed at a previous employer. The reasoning was probably sound; the premise — that this is the shape of thing your platform offers — is what's wrong, and he had no way to know.
Here is the property that makes the distinction operational rather than philosophical. A requirement can fail. An implementation proposal cannot.
"Our licence server must never show more concurrent seats than we've paid for" is a statement about the world that can be true or false on a given Tuesday. You can measure it. You can be sued over it. "Run a script at login" cannot be true or false; it can only be present or absent. That's the test I use in the moment, and it's fast enough to run live: can what they just said be violated? If not, it's a proposal, and there's a requirement one layer down that I don't have yet.
This matters commercially, and it's worth saying to your own account team: requirements are far less varied than proposals. Six customers will ask for six different extension mechanisms and turn out to have three requirements between them. Proposals diverge because they were designed independently against different prior products; requirements converge because the underlying business problems are common. Build to proposals and you build six things. Build to requirements and you build three, reusable.
The four questions that actually surface it
"Why do you need that?" doesn't work. It reads as adversarial, and the honest answer to it is "because that's how we planned to do it," which just gets you the proposal again, louder. Four questions work better, and each one does a different job.
"What goes wrong if this doesn't happen?" Names the failure being prevented rather than the mechanism preventing it. The answer is the requirement, or one question away from it. If it's vague — "it just wouldn't be right" — you've found a request with no requirement behind it, usually someone reproducing a previous architecture out of habit.
"How quickly does a change have to take effect?" The highest-information question available and the one most engineers skip. It separates requests that must be inside the authentication transaction from the majority that only need to be eventually true. "Immediately" turns out to mean "within a few minutes" most of the time; the exceptions are the ones worth taking seriously.
"What does the answer change?" For anything shaped like a check — fraud, risk, entitlement, licence — ask what the system does differently with the result. "We'd step up to MFA" means you're being asked for a policy input. "We'd block" invites the follow-up: how often? A check that blocks one authentication in ten thousand while adding 200ms to all of them is a trade someone should make consciously.
"Who owns the code on the other side, and can they change it?" Distinguishes an immovable constraint (a 2009 application whose vendor is out of business) from a negotiable one (a team in the same building who'd rather not).
The four shapes an answer can take
Before the worked examples, the vocabulary. Assume a platform that does not execute customer code inside the identity runtime — the reasoning for that constraint is The Extensibility Trap and I'm not going to re-argue it here. Given that constraint, there are four sanctioned shapes an answer can take, and one honest fifth.
| Shape | What it is | Preserves | Costs |
|---|---|---|---|
| Declarative mapping | A constrained expression over data the system already holds: rename, concatenate, conditionally include, reformat | Determinism, latency budget, isolation | Has a ceiling; no I/O, no external lookup |
| Policy expression | Terminating rules over request context and local attributes, producing a decision | All of the above | Can only reason about facts already present |
| Async event / webhook | The platform publishes; the customer's system consumes on its own schedule | The critical path — nothing waits | Cannot change the outcome of this authentication |
| Pre-sync / pre-compute | The external fact is pushed or pulled before login (SCIM, scheduled sync, a signal feed) and read locally | Everything; the dependency moves off the hot path | Staleness window; integration work in the customer's infrastructure |
| (Build it for everyone) | It's a product feature, not an extension | — | Roadmap, and permanence |
The discriminator is a two-by-two, and once you see it most requests classify immediately:
| Input is already inside the identity system | Input lives in an external system | |
|---|---|---|
| Must change the outcome of this authentication | Policy expression | Pre-sync — and if it genuinely can't be pre-synced, this is the cell where you may have to lose the deal |
| Is a consequence of the authentication | Declarative mapping (shaping what you emit) | Async event |
The top-right cell is the only hard one, and every painful conversation lives there. The craft is establishing honestly whether a request that appears to sit there actually belongs in one of the other three.
Six translations
Consistent format. What they asked for, what they need, what you offer, what they lose, and — the part that keeps you honest — when the right answer is just yes.
1. Run custom code at login
Asked for: "Upload a script that runs during authentication."
Actually needed: Their licence agreement with a third-party vendor caps concurrent named users, and overage is billed punitively. The script was going to count active sessions and refuse the login.
Surfaced by: "What goes wrong if this doesn't happen?" — followed by an actual number, which is the tell that you've reached a requirement.
Offer instead: A concurrent-session limit as a policy primitive, evaluated locally against session state you already own, plus an async session event stream so their licence system stays reconciled. The script never needed to be a script; it needed a counter and a threshold, both facts already inside your system. A top-left request wearing a top-right costume.
They genuinely lose: Hard admission control. A local session count is subject to a race — two logins arriving within the same few hundred milliseconds across two regions can both observe N-1. If their contract has a per-seat penalty measured per event, that race is a real exposure and you should say so rather than hand-wave it.
Just say yes when: The cap is contractual with financial teeth and they need strict enforcement. But "yes" here means build concurrent session limiting as a product feature, with a documented consistency model — not run their script. Two customers with seat caps is a roadmap item; two customers asking to upload scripts is not.
2. A claim enriched from an external system
Asked for: "Call SAP during token issuance and put the user's cost centre in the token."
Actually needed: A downstream expense application authorizes on cost centre and has no way to look it up itself.
Surfaced by: "How quickly does a change have to take effect?" — cost centres change during reorganisations, at most monthly, and the customer's own HR process takes three days to propagate anyway. They asked for a synchronous call to keep something fresh that is structurally stale by seventy-two hours upstream of you.
Offer instead: Pre-sync. SCIM or a scheduled push makes cost centre a local attribute and the login path does a local read — same value in the token, entirely different failure characteristics. Then ask the better question underneath: does the token need it at all, or does the expense app need an API it can call? A business attribute in a token means every consumer's authorization decision depends on your refresh cadence, which is the argument in Keep Business Logic Out of Identity.
They genuinely lose: A staleness window, which needs a number attached — "up to four hours" is a design, "eventually" is an argument later. They also inherit an obligation: the downstream app must behave sanely when the attribute is absent, because during initial sync and after failures it will be. A synchronous fetch has that problem too; it just hides it behind a timeout.
Just say yes when: The value is genuinely a property of this authentication rather than of the user — which contract they selected at the login prompt, which of four tenants they chose. That isn't enrichment. That's part of the authentication event, and it belongs in the token.
3. A bespoke SAML attribute format
Asked for: Group memberships as a single semicolon-delimited string, in an attribute named memberOfString, uppercased, with the CN= prefix stripped.
Actually needed: Exactly that. There's a 2009 application whose parser expects it, the vendor was acquired twice, and nobody can change it.
Surfaced by: "Who owns the code on the other side?" — answer: nobody, in the strong sense.
Offer instead: Say yes, via declarative mapping. Worth naming clearly, because engineers who have internalised the no-plugins rule sometimes over-apply it. Pure transformation of data you already hold: no I/O, terminating, bounded. It is configuration in the strict sense from Configuration Beats Customization — a point in a space you designed, not a new dimension.
They genuinely lose: Very little; the honest loss is a ceiling. Your expression language won't do everything, and the interesting question is what happens at the edge. That failure should be a validation error at configuration time with a comprehensible message, not a runtime surprise in an assertion at 3am. A constrained language whose limits are discoverable is a feature; one whose limits are discovered in production is a plugin API with worse ergonomics — and every extension point is a permanent public API whether you called it one or not.
Just say yes when: Essentially always, for this class. The discipline is in the language design, not the answer.
4. Per-tenant password policy
Asked for: "We need to configure our own password rules — our standard is 14 characters, 90-day rotation, and no more than two repeated characters."
Actually needed: To pass an audit. Someone is going to ask them for evidence that a stated policy is enforced.
Surfaced by: "What document are you being audited against, and what does the auditor ask to see?" This question reframes the whole conversation, because the answer is usually a control statement plus a screenshot, and it reveals that the requirement is evidence, not any particular rule.
Offer instead: Two things, and keeping them separate is the point. First, per-tenant policy selection from a designed set — a point on an existing axis, which is cheap. Second, an exportable statement of a tenant's effective policy with a timestamp and change history, which is what the auditor actually wants and which almost nobody ships. Nobody asks for the second because nobody has offered it.
Where to hold the line: "per-tenant policy" is a request for an axis, and axes are fine. "Let us supply an arbitrary regex" is a request for a new dimension, plus a support obligation for the catastrophically backtracking pattern someone will eventually paste in. The two sound identical on a call and are not remotely the same commitment. (On whether the rules themselves are worth much: Compliance Still Requires 90-Day Rotation and The Password Policy Arms Race.)
They genuinely lose: Any rule outside your policy vocabulary — the repeated-characters one, probably. Say that plainly and check whether it's actually in their control document or was inherited from a system they're replacing. Frequently it's the latter, and it dies quietly once someone looks.
Just say yes when: The second customer asks for the same shape. Then design the axis properly: name it, enumerate valid values, pick a default, test them. That converts an unbounded dimension into a bounded one for everyone who comes after.
5. A synchronous fraud check at authentication
Asked for: "Before you issue the token, call our risk API. If it returns high, don't."
Actually needed: This is the hardest one, and the one where you should be least confident, because sometimes synchronous really is the requirement.
Surfaced by: "What does the answer change?" The answers split three ways. "We'd require MFA" is a request for a policy input, not a plugin. "We'd block" needs a base rate and a real latency budget. "We'd log it" is an async event and always was.
Offer instead: Invert the direction. Their risk engine pushes per-subject signals to you continuously — a score, a flag, a device reputation — and you project it as a local attribute that policy reads at zero I/O cost. Pair that with an async authentication event and a session-revocation API, so a post-hoc high-risk verdict kills the session within seconds rather than preventing it milliseconds earlier.
The mechanism argument to make out loud, because it lands with their engineers rather than their executives: a synchronous call to your risk API has no correct failure mode. When it times out we either issue anyway — and the control is silently absent during exactly the incident it was built for — or we refuse, and your risk service's bad afternoon becomes a company-wide login outage. Both answers are wrong. That's the shape of the dependency, not a policy position, and it doesn't improve with a better SLA.
They genuinely lose: Signal freshness bounded by the push interval, and a window between issuance and revocation. Both are numbers. Give them the numbers.
Just say yes when: Money moves inside that window and the window is the loss. But notice what that implies — the check belongs at the transaction, not at the login, because that's where the money is and where the relevant context exists. Authentication happens once and a payment happens two hours later; a risk decision made at login is stale by the time it matters. See Identity Isn't Your Fraud Engine, and Step-Up Authentication Done Right for the mechanism that usually resolves this properly. If their signal is geo-velocity, Impossible Travel Is Full of False Positives is worth sending ahead of the call.
6. Store their downstream API keys
Asked for: "You already hold secrets. Hold ours — our service needs a vendor API key, inject it into the token."
Actually needed: A service (increasingly, an agent) must call a third-party API, and they don't want to build secret distribution.
Surfaced by: "Who is this credential for — your service, or the user?" The answers lead to completely different places, and the request usually conflates them.
Offer instead: If the downstream speaks OAuth, this is delegation, and the primitive is RFC 8693 token exchange: the subject stays the original user, each actor accumulates in the act chain, and no long-lived secret is copied anywhere — see Token Exchange: The Grant Type Nobody Teaches. If the downstream is a 2011 SaaS with a static key in a header, the answer is a secrets manager they own, because a static third-party credential held in an identity platform is a secret store wearing a costume — and see Why API Keys Refuse to Die on why this keeps coming up.
Disclosure: I work on ClavionX, whose machine-identity model is in design rather than shipped — this is an intended shape, not a maturity claim. The design treats an agent as a first-class registered object with an owner, a stated purpose, and a lifecycle (REGISTERED → ACTIVE → SUSPENDED → RETIRED), realized one-to-one as an OAuth2 client, with an MCP server modelled as a resource server. What that buys in this conversation is that "hold our API key" has somewhere else to go: the agent is an identity that can be issued short-lived credentials and delegated user authority, rather than a process needing a copy of a secret. It doesn't help when the third party only accepts a static string. Nothing does — that's a fact about the third party.
They genuinely lose: Real work, if the downstream can't accept a token — they need a broker in their own infrastructure. Don't pretend otherwise.
Just say yes when: The credential is one you issued. Managing your own client secrets and rotating them is your job, and refusing that under a "we're not a secrets manager" banner is using a principle to avoid work.
The summary you can send afterwards
| They asked for | The requirement underneath | The shape |
|---|---|---|
| Custom code at login | Enforce a concurrent-seat cap | Policy + product feature |
| Enrich a claim from SAP | A downstream app needs an attribute | Pre-sync |
| Bespoke SAML attribute format | An unchangeable legacy parser | Declarative mapping |
| Per-tenant password policy | Evidence for an auditor | Configuration axis + policy export |
| Synchronous fraud check | Block or challenge risky sessions | Signal pre-sync + async revoke |
| Store downstream API keys | A service must call a third party | Token exchange, or their secrets manager |
Which requirements you should build for everyone
The translations above dodge the harder question. Some of these requirements are genuine product gaps, and declining them is the actual mistake. Four tests, in increasing order of how much I trust them.
Can you write the documentation sentence without naming a customer? If the doc has to say "for customers who use a legacy licence server," it isn't a product feature yet; it's a special case with better marketing.
Is it a point on an existing axis, or a new axis? Points are roughly linear; axes multiply. The most mechanical test available, and the whole argument of Configuration Beats Customization.
Is the second request for the same requirement, or merely the same proposal? The refinement I'd most want people to take away. "Two customers asked for scripting hooks" is not two data points — it's two customers independently reaching for the same generic escape hatch for unrelated reasons, and the hook satisfies neither well. "Two customers need to cap concurrent seats" is two data points on one requirement, and that's a feature.
If this customer churned tomorrow, would you keep it? The best test I know, because it isolates the requirement from the revenue. The Hidden Cost of Customer-Specific Features is the accounting of what failing it costs later.
There's a fifth that's a veto rather than a test: does the feature preserve determinism, a budgeted latency, tenant isolation, and independence from external availability? A requirement can pass all four tests above and still be one you decline, because satisfying it costs a property the platform is built on.
The answer that costs the most
"We'll put it behind a flag, just for you" is the most expensive sentence available on that call, and it's expensive in a way that is invisible at the moment it's said. I won't re-tell that story — it's here, including why the flag outlived the customer by three years.
The part worth adding is about the call itself. The flag is attractive because it converts an architecture decision into a commercial one at exactly the moment when nobody in the room is positioned to price it. You stop being the obstacle, the AE relaxes, and it feels like collaboration.
And a second thing happens that nobody tracks: the customer does not hear "a configuration value." They hear a commitment to a behaviour. It goes into their integration tests, their runbook, their architecture doc. You have created an undocumented API with one consumer, no specification, and no deprecation path, whose only record is a row in a config table and a memory in a Slack thread. Two years later someone refactors and breaks it, and the incident review can't establish whether it was ever supported.
If you're going to do it anyway — and sometimes you should — write down, that day, who asked, what breaks without it, and what condition would let it die. That information exists only on the day it's created.
Writing a no that survives being forwarded
Whatever you say on the call gets compressed into an email, and that email gets forwarded to someone who wasn't there — usually the customer's CTO, usually with two lines of context above it. Write for that reader. Seven properties, all of which I've seen matter.
1. Lead with their requirement, stated back more precisely than they stated it. If your first paragraph is about your architecture, it will be forwarded as "the vendor said no." If it's a sharper articulation of their own problem than they managed, it gets forwarded as "they understand this." Same content, different first paragraph, entirely different meeting downstream.
2. Give the mechanism, not the policy. "We don't support customer code" is a policy, and policies have exception approvers — you have just told a CTO to find yours. "Executing your code inside token issuance makes our authentication availability the product of your HR system's availability, and gives us no correct behaviour when your code throws: we either issue a token with a missing attribute your app treats as authorization, or we fail the login" is a mechanism. Mechanisms don't have exception approvers. Nobody can escalate their way around arithmetic.
3. Show the failure mode you are both avoiding, with both wrong answers side by side. The fail-open/fail-closed framing does more work than any other rhetorical move available, because it demonstrates that you looked for a way to say yes and found that the position has no correct behaviour in it. That's a different claim from "we'd rather not."
4. Include a design, not a direction. Endpoints, data shapes, sync intervals, who runs what, what happens when it's down. A no with a concrete integration diagram is a proposal; a no with an apology is a refusal. This is the largest difference between emails that work and emails that don't, and it's mostly a question of effort.
5. State what they lose, in their favour. Claim your alternative is strictly better and a competent CTO will find the case where it isn't — the race window, the staleness gap — and then every other claim is suspect. Naming the loss yourself is what makes the rest credible. It's also just true.
6. Say what would change the answer. "If your risk engine can't push signals, we should talk again, because that's the case this design doesn't cover." A no with a condition attached is a position. One without is a wall.
7. Never route the argument through pricing. "That's available on our enterprise tier" converts a design conversation into a procurement conversation, and you will lose that one — you've just conceded that the objection was never technical.
One practical note: send it before the follow-up call, not after. A written argument people have read individually gets engaged with on its merits; the same argument delivered live gets responded to socially, by people managing a room.
Counterpoint: the times you're the one who's wrong
An article that only ever says no is a temperament, not an argument. Four ways this discipline goes bad.
Your alternative is a cost transfer, and you should be able to price it. Pre-sync doesn't remove work; it moves it into the customer's infrastructure, where they operate a SCIM connector, monitor it, and get paged when it breaks. If you can't say roughly what that costs to build and run, you haven't finished the analysis — you've relocated the problem somewhere you don't have to look at it. Sometimes the honest reckoning is that the plugin is cheaper in total and you're declining for reasons about your product's properties rather than their economics. That's defensible, but it's a different argument, and saying it plainly earns more trust than pretending the alternative is free.
Repeated "not that way, here's how" is telemetry about your configuration surface. If the "how" is always work for them, your intent vocabulary is too thin — the failure mode in Why Enterprise Capabilities Shouldn't Require Enterprise Complexity. Feature requests are the best signal you get about where your configuration model doesn't reach, and treating them purely as things to deflect wastes the data. Log the requirement from every one of these calls, not the request. The clusters are your roadmap.
Sometimes the plugin request means you're eighteen months behind. If three customers independently need something and your answer to all three is a bespoke integration, the market has told you what to build. The no-plugins position is only honest if it comes with a real commitment to close the gaps it creates; otherwise it's an excuse dressed as a principle.
And sometimes the right answer is to lose the deal. Not as a posture — as arithmetic. A deal is one-time revenue against a recurring cost, and the discount rate people apply to that recurring cost is close to zero because it's paid by a different team in a different year. Losing is correct when the requirement contradicts a design premise rather than extending it, when the customer's cost structure is unlike the rest of your base (why enterprise customers cost more), or — the clearest signal there is — when you can only say yes by making a promise that engineering hasn't agreed to. If the yes requires you to be vague, it's a no you haven't admitted yet.
Losing well matters more than most engineers realise. Say it early, say it in mechanism terms, and hand over the reference architecture anyway. Deals lost cleanly in month two come back in year two, usually when the vendor who said yes has a bad quarter. Deals lost in month nine, after a proof of concept built on a promise nobody could keep, do not.
The line
The engineer on that call did real work and arrived with a design. Treating it as a design — engaging with it, finding the requirement inside it, returning a better one — is a completely different act from refusing a request.
Ask what would break. Ask how fast it has to be. Ask what the answer changes. Then answer the requirement, say precisely what they're giving up, and write it so it survives being forwarded to someone who wasn't in the room.
The implementation was never the ask. It was just the only shape they had for it.