chore(workflow): temporarily lift credit-cap gate on workflow steps (#19904)

## Summary

- Removes the per-step `canBillMeteredProduct(WORKFLOW_NODE_EXECUTION)`
gate in `WorkflowExecutorWorkspaceService.executeStep` so workflows keep
running when a workspace reaches `hasReachedCurrentPeriodCap`.
Previously every step failed with
`BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE` (\"No remaining credits to
execute workflow…\").
- Drops the now-unused `BillingService` injection, related imports, and
the helper `canBillWorkflowNodeExecution`. Updates the spec to drop the
corresponding billing-validation case and mock.
- Leaves the constant file and `BillingService` itself in place, plus a
TODO at the previous gate site, so the behavior can be re-enabled with a
small, reviewable revert.

## Notes

- Usage events are still emitted (`USAGE_RECORDED` /
`UsageResourceType.WORKFLOW`), and `EnforceUsageCapJob` keeps computing
the cap and flipping `hasReachedCurrentPeriodCap` — only the executor
stops consulting that flag.
- The runner-level `canFeatureBeUsed` check in
`WorkflowRunnerWorkspaceService.run` was already log-only (subscription
presence, not credits), so no change there.
- AI chat (`agent-chat.resolver.ts`) keeps its own
`BILLING_CREDITS_EXHAUSTED` gate; this PR does not touch it.

## Test plan

- [x] `npx jest workflow-executor.workspace-service.spec.ts` (17/17
pass)
- [ ] Manual: with billing enabled and the metered subscription item
flagged `hasReachedCurrentPeriodCap = true`, trigger a workflow run and
verify steps execute end-to-end instead of failing with the billing
error.

Made with [Cursor](https://cursor.com)
This commit is contained in:
Charles Bochet
2026-04-21 01:00:28 +02:00
committed by GitHub
parent 57de05ea74
commit 1b469168c8
2 changed files with 3 additions and 66 deletions
@@ -2,8 +2,6 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { getWorkflowRunContext, StepStatus } from 'twenty-shared/workflow';
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
@@ -71,11 +69,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
getWorkflowRunOrFail: jest.fn(),
};
const mockBillingService = {
isBillingEnabled: jest.fn().mockReturnValue(true),
canBillMeteredProduct: jest.fn().mockReturnValue(true),
};
const mockExceptionHandlerService = {
captureExceptions: jest.fn(),
};
@@ -108,10 +101,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
provide: WorkflowRunWorkspaceService,
useValue: mockWorkflowRunWorkspaceService,
},
{
provide: BillingService,
useValue: mockBillingService,
},
{
provide: ExceptionHandlerService,
useValue: mockExceptionHandlerService,
@@ -341,39 +330,6 @@ describe('WorkflowExecutorWorkspaceService', () => {
);
});
it('should stop when billing validation fails', async () => {
mockBillingService.isBillingEnabled.mockReturnValueOnce(true);
mockBillingService.canBillMeteredProduct.mockReturnValueOnce(false);
await service.executeFromSteps({
workflowRunId: mockWorkflowRunId,
stepIds: ['step-1'],
workspaceId: mockWorkspaceId,
});
expect(workflowActionFactory.get).toHaveBeenCalledTimes(0);
expect(
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
).toHaveBeenCalledTimes(1);
expect(workflowRunWorkspaceService.endWorkflowRun).toHaveBeenCalledTimes(
1,
);
expect(
workflowRunWorkspaceService.updateWorkflowRunStepInfo,
).toHaveBeenCalledWith({
stepId: 'step-1',
stepInfo: {
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
status: StepStatus.FAILED,
},
workflowRunId: mockWorkflowRunId,
workspaceId: 'workspace-id',
});
});
it('should not emit billing event for skipped steps', async () => {
(shouldExecuteStep as jest.Mock).mockReturnValue(false);
(shouldSkipStepExecution as jest.Mock).mockReturnValue(true);
@@ -9,9 +9,6 @@ import {
WorkflowRunStepInfos,
} from 'twenty-shared/workflow';
import { BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE } from 'src/engine/core-modules/billing/constants/billing-workflow-execution-error-message.constant';
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@@ -59,7 +56,6 @@ export class WorkflowExecutorWorkspaceService {
private readonly workflowActionFactory: WorkflowActionFactory,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
private readonly billingService: BillingService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly metricsService: MetricsService,
@InjectMessageQueue(MessageQueue.workflowQueue)
@@ -376,16 +372,6 @@ export class WorkflowExecutorWorkspaceService {
);
}
private async canBillWorkflowNodeExecution(workspaceId: string) {
return (
!this.billingService.isBillingEnabled() ||
(await this.billingService.canBillMeteredProduct(
workspaceId,
BillingProductKey.WORKFLOW_NODE_EXECUTION,
))
);
}
private async processStepExecutionResult({
actionOutput,
stepId,
@@ -467,14 +453,9 @@ export class WorkflowExecutorWorkspaceService {
workflowRunId: string;
workspaceId: string;
}) {
const canBill = await this.canBillWorkflowNodeExecution(workspaceId);
if (!canBill) {
return {
error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE,
};
}
// TODO: re-enable workflow node execution credit cap once billing limits are revisited.
// Previously gated on BillingService.canBillMeteredProduct(WORKFLOW_NODE_EXECUTION);
// temporarily disabled so workflows keep running when the period cap is reached.
const stepId = step.id;
const workflowAction = this.workflowActionFactory.get(step.type);