refactor: replace adaptive_caching with code_version, remove code_v2 (#5227)

Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
pedrohsdb 2026-03-24 18:10:47 -07:00 committed by GitHub
parent a5a8172b8a
commit 4d04d8eb55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 213 additions and 172 deletions

View file

@ -0,0 +1,31 @@
"""code_version db migration
Revision ID: 787109c06571
Revises: 01dbfbf87496
Create Date: 2026-03-25 01:06:29.590429+00:00
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "787109c06571"
down_revision: Union[str, None] = "01dbfbf87496"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("workflows", sa.Column("code_version", sa.Integer(), server_default=sa.text("2"), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("workflows", "code_version")
# ### end Alembic commands ###

View file

@ -190,7 +190,7 @@ type RunWorkflowRequestBody = {
max_screenshot_scrolls?: number | null;
extra_http_headers?: Record<string, string> | null;
browser_address?: string | null;
run_with?: "agent" | "code" | "code_v2";
run_with?: "agent" | "code";
ai_fallback?: boolean;
};
@ -265,18 +265,17 @@ function transformToWorkflowRunRequest(
return transformed;
}
const VALID_RUN_WITH = new Set(["agent", "code", "code_v2"]);
const VALID_RUN_WITH = new Set(["agent", "code"]);
function deriveRunWith(
workflow?: WorkflowApiResponse,
override?: string | null,
): "agent" | "code" | "code_v2" {
): "agent" | "code" {
if (override && VALID_RUN_WITH.has(override))
return override as "agent" | "code" | "code_v2";
if (workflow?.run_with === "code_v2") return "code_v2";
if (workflow?.adaptive_caching && workflow?.run_with === "code")
return "code_v2";
return override as "agent" | "code";
if (workflow?.run_with === "agent") return "agent";
if (workflow?.run_with === "code") return "code";
if ((workflow?.code_version ?? 0) >= 1) return "code";
return "agent";
}
@ -287,7 +286,7 @@ type RunWorkflowFormType = Record<string, unknown> & {
cdpAddress: string | null;
maxScreenshotScrolls: number | null;
extraHttpHeaders: string | null;
runWith: "agent" | "code" | "code_v2";
runWith: "agent" | "code";
aiFallback: boolean | null;
};
@ -394,15 +393,6 @@ function RunWorkflowForm({
status: "published",
});
const { data: blockScriptsV2 } = useBlockScriptsQuery({
cacheKey,
cacheKeyValue: cacheKeyValue ? `${cacheKeyValue}:v2` : "v2",
workflowPermanentId,
status: "published",
});
const hasCodeV2 = Object.keys(blockScriptsV2 ?? {}).length > 0;
const [hasCode, setHasCode] = useState(false);
useEffect(() => {
@ -569,25 +559,6 @@ function RunWorkflowForm({
} satisfies ApiCommandOptions;
}}
/>
{hasCodeV2 && (
<Button
type="button"
variant="outline"
disabled={
runWorkflowMutation.isPending || hasLoginBlockValidationError
}
onClick={() => {
form.setValue("runWith", "code_v2");
form.handleSubmit(onSubmit, handleInvalid)();
}}
>
<PlayIcon className="mr-2 h-4 w-4" />
Run with Code 2.0
<span className="ml-2 rounded bg-amber-500/20 px-1.5 py-0.5 text-xs font-semibold text-amber-400">
Beta
</span>
</Button>
)}
<Button
type="submit"
disabled={
@ -910,12 +881,6 @@ function RunWorkflowForm({
generated).
</span>
),
code_v2: (
<span>
Run this workflow with Code 2.0 (adaptive caching with
self-healing scripts).
</span>
),
};
return (
<FormItem>
@ -942,7 +907,6 @@ function RunWorkflowForm({
<SelectContent>
<SelectItem value="agent">Skyvern Agent</SelectItem>
<SelectItem value="code">Code</SelectItem>
<SelectItem value="code_v2">Code 2.0</SelectItem>
</SelectContent>
</Select>
</FormControl>

View file

@ -112,7 +112,7 @@ const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
title: "New Workflow",
description: "",
ai_fallback: true,
adaptive_caching: true,
code_version: 2,
run_with: "code",
workflow_definition: {
version: 2,

View file

@ -8,7 +8,7 @@ const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
title: "New Workflow",
description: "",
ai_fallback: true,
adaptive_caching: true,
code_version: 2,
run_with: "code",
workflow_definition: {
version: 2,

View file

@ -124,10 +124,8 @@ function getWorkflowElements(version: WorkflowVersion) {
extraHttpHeaders: version.extra_http_headers
? JSON.stringify(version.extra_http_headers)
: null,
runWith:
version.adaptive_caching && version.run_with === "code"
? "code_v2"
: version.run_with,
runWith: version.run_with,
codeVersion: version.code_version ?? null,
scriptCacheKey: version.cache_key,
aiFallback: version.ai_fallback ?? true,
runSequentially: version.run_sequentially ?? false,

View file

@ -451,13 +451,13 @@ export function WorkflowCopilotChat({
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
totp_verification_url: saveData.workflow.totp_verification_url,
extra_http_headers: extraHttpHeaders,
run_with:
saveData.settings.runWith === "code_v2"
? "code"
: saveData.settings.runWith,
run_with: saveData.settings.runWith,
cache_key: normalizedKey,
ai_fallback: saveData.settings.aiFallback ?? true,
adaptive_caching: saveData.settings.runWith === "code_v2",
code_version:
saveData.settings.runWith === "code"
? saveData.settings.codeVersion ?? 2
: undefined,
workflow_definition: {
version: saveData.workflowDefinitionVersion,
parameters: saveData.parameters,

View file

@ -82,10 +82,8 @@ function Debugger() {
extraHttpHeaders: workflow.extra_http_headers
? JSON.stringify(workflow.extra_http_headers)
: null,
runWith:
workflow.adaptive_caching && workflow.run_with === "code"
? "code_v2"
: workflow.run_with,
runWith: workflow.run_with,
codeVersion: workflow.code_version ?? null,
scriptCacheKey: workflow.cache_key,
aiFallback: workflow.ai_fallback ?? true,
runSequentially: workflow.run_sequentially ?? false,

View file

@ -74,10 +74,8 @@ function WorkflowEditor() {
extraHttpHeaders: workflow.extra_http_headers
? JSON.stringify(workflow.extra_http_headers)
: null,
runWith:
workflow.adaptive_caching && workflow.run_with === "code"
? "code_v2"
: workflow.run_with ?? "agent",
runWith: workflow.run_with ?? "agent",
codeVersion: workflow.code_version ?? null,
scriptCacheKey: workflow.cache_key,
aiFallback: workflow.ai_fallback ?? true,
runSequentially: workflow.run_sequentially ?? false,

View file

@ -1131,10 +1131,8 @@ function Workspace({
extraHttpHeaders: workflowData.extra_http_headers
? JSON.stringify(workflowData.extra_http_headers)
: null,
runWith:
workflowData.adaptive_caching && workflowData.run_with === "code"
? "code_v2"
: workflowData.run_with ?? "agent",
runWith: workflowData.run_with ?? "agent",
codeVersion: workflowData.code_version ?? null,
scriptCacheKey: workflowData.cache_key ?? null,
aiFallback: workflowData.ai_fallback ?? true,
runSequentially: workflowData.run_sequentially ?? false,
@ -1181,10 +1179,8 @@ function Workspace({
extraHttpHeaders: selectedVersion.extra_http_headers
? JSON.stringify(selectedVersion.extra_http_headers)
: null,
runWith:
selectedVersion.adaptive_caching && selectedVersion.run_with === "code"
? "code_v2"
: selectedVersion.run_with ?? "agent",
runWith: selectedVersion.run_with ?? "agent",
codeVersion: selectedVersion.code_version ?? null,
scriptCacheKey: selectedVersion.cache_key,
aiFallback: selectedVersion.ai_fallback ?? true,
runSequentially: selectedVersion.run_sequentially ?? false,
@ -1979,13 +1975,14 @@ function Workspace({
created_at: new Date().toISOString(),
modified_at: new Date().toISOString(),
deleted_at: null,
run_with:
saveData.settings.runWith === "code_v2"
? "code"
: saveData.settings.runWith,
run_with: saveData.settings.runWith,
cache_key: saveData.settings.scriptCacheKey,
ai_fallback: saveData.settings.aiFallback,
adaptive_caching: saveData.settings.runWith === "code_v2",
adaptive_caching: false,
code_version:
saveData.settings.runWith === "code"
? saveData.settings.codeVersion ?? 2
: null,
run_sequentially: saveData.settings.runSequentially,
sequential_key: saveData.settings.sequentialKey,
folder_id: null,

View file

@ -307,12 +307,6 @@ function StartNode({ id, data, parentId }: NodeProps<StartNode>) {
Skyvern Agent
</SelectItem>
<SelectItem value="code">Code</SelectItem>
<SelectItem value="code_v2">
<span>Code 2.0</span>{" "}
<span className="text-xs italic text-yellow-400">
new
</span>
</SelectItem>
</SelectContent>
</Select>
</div>

View file

@ -13,6 +13,7 @@ export type WorkflowStartNodeData = {
extraHttpHeaders: string | Record<string, unknown> | null;
editable: boolean;
runWith: string | null;
codeVersion: number | null;
scriptCacheKey: string | null;
aiFallback: boolean;
runSequentially: boolean;

View file

@ -156,10 +156,8 @@ function getWorkflowElements(version: WorkflowVersion) {
extraHttpHeaders: version.extra_http_headers
? JSON.stringify(version.extra_http_headers)
: null,
runWith:
version.adaptive_caching && version.run_with === "code"
? "code_v2"
: version.run_with,
runWith: version.run_with,
codeVersion: version.code_version ?? null,
scriptCacheKey: version.cache_key,
aiFallback: version.ai_fallback ?? true,
runSequentially: version.run_sequentially ?? false,

View file

@ -1534,6 +1534,7 @@ function getElements(
extraHttpHeaders: settings.extraHttpHeaders,
editable,
runWith: settings.runWith,
codeVersion: settings.codeVersion,
scriptCacheKey: settings.scriptCacheKey,
aiFallback: settings.aiFallback ?? true,
label: "__start_block__",
@ -2779,7 +2780,8 @@ function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
model: null,
maxScreenshotScrolls: null,
extraHttpHeaders: null,
runWith: "code_v2",
runWith: "code",
codeVersion: 2,
scriptCacheKey: null,
aiFallback: true,
runSequentially: false,
@ -2806,6 +2808,7 @@ function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
? JSON.stringify(data.extraHttpHeaders)
: data.extraHttpHeaders,
runWith: data.runWith,
codeVersion: data.codeVersion,
scriptCacheKey: data.scriptCacheKey,
aiFallback: data.aiFallback,
runSequentially: data.runSequentially,
@ -4034,6 +4037,7 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
status: workflow.status,
run_with: workflow.run_with,
adaptive_caching: workflow.adaptive_caching ?? undefined,
code_version: workflow.code_version ?? undefined,
cache_key: workflow.cache_key,
ai_fallback: workflow.ai_fallback ?? undefined,
run_sequentially: workflow.run_sequentially ?? undefined,

View file

@ -600,10 +600,11 @@ export type WorkflowApiResponse = {
created_at: string;
modified_at: string;
deleted_at: string | null;
run_with: string | null; // 'agent', 'code', or 'code_v2'
run_with: string | null; // 'agent' or 'code'
cache_key: string | null;
ai_fallback: boolean | null;
adaptive_caching: boolean | null;
code_version: number | null;
run_sequentially: boolean | null;
sequential_key: string | null;
folder_id: string | null;
@ -617,7 +618,8 @@ export type WorkflowSettings = {
model: WorkflowModel | null;
maxScreenshotScrolls: number | null;
extraHttpHeaders: string | null;
runWith: string | null; // 'agent', 'code', or 'code_v2'
runWith: string | null; // 'agent' or 'code'
codeVersion: number | null;
scriptCacheKey: string | null;
aiFallback: boolean | null;
runSequentially: boolean;

View file

@ -19,6 +19,7 @@ export type WorkflowCreateYAMLRequest = {
cache_key?: string | null;
ai_fallback?: boolean;
adaptive_caching?: boolean;
code_version?: number | null;
run_sequentially?: boolean;
sequential_key?: string | null;
folder_id?: string | null;

View file

@ -77,9 +77,6 @@ function WorkflowRunCode(props?: Props) {
workflowRunId: workflowRun?.workflow_run_id,
});
const isAdaptiveCaching =
workflow?.adaptive_caching && workflow?.run_with === "code";
useEffect(() => {
const keys = Object.keys(blockScriptsPublished?.blocks ?? {});
setHasPublishedCode(
@ -127,15 +124,15 @@ function WorkflowRunCode(props?: Props) {
? selectedVersionCode
: activeScripts;
// For non-adaptive-caching, use block labels from the displayed version
// Use block labels from the displayed version when viewing an older version
// (older versions may have different blocks than the current run)
const displayBlockLabels = isViewingOtherVersion
? Object.keys(displayScripts?.blocks ?? {})
: orderedBlockLabels;
// For adaptive caching, prefer the full main.py script over stitched blocks
// Prefer the full main_script when available, fall back to stitched blocks
const code = (
isAdaptiveCaching && displayScripts?.main_script
displayScripts?.main_script
? displayScripts.main_script
: getCode(displayBlockLabels, displayScripts?.blocks).join("")
).trim();

View file

@ -152,13 +152,13 @@ const useWorkflowSave = (opts?: WorkflowSaveOpts) => {
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
totp_verification_url: saveData.workflow.totp_verification_url,
extra_http_headers: extraHttpHeaders,
run_with:
saveData.settings.runWith === "code_v2"
? "code"
: saveData.settings.runWith,
run_with: saveData.settings.runWith,
cache_key: normalizedKey,
ai_fallback: saveData.settings.aiFallback ?? true,
adaptive_caching: saveData.settings.runWith === "code_v2",
code_version:
saveData.settings.runWith === "code"
? saveData.settings.codeVersion ?? 2
: undefined,
workflow_definition: {
version: saveData.workflowDefinitionVersion,
parameters: saveData.parameters,

View file

@ -475,7 +475,7 @@ def _validate_definition_structure(json_def: WorkflowCreateYamlRequest | None, a
_CODE_V2_DEFAULTS: dict[str, Any] = {
"adaptive_caching": True,
"code_version": 2,
"run_with": "code",
}
_DEFAULT_MCP_PROXY_LOCATION = ProxyLocation.RESIDENTIAL
@ -603,7 +603,7 @@ def _inject_missing_top_level_defaults(definition: str, fmt: str, defaults: dict
def _inject_code_v2_defaults(definition: str, fmt: str) -> str:
"""Inject Code 2.0 defaults into a JSON definition string when not explicitly set.
"""Inject Code 2.0 defaults (code_version=2, run_with=code) when not explicitly set.
Only modifies JSON definitions (or auto-detected JSON). YAML is returned unchanged.
"""
@ -827,8 +827,8 @@ async def skyvern_workflow_create(
"""Create a new Skyvern workflow from a YAML or JSON definition. Use when you need to save
a new automation workflow that can be run repeatedly with different parameters.
By default, workflows created via MCP use Code 2.0 (adaptive caching with run_with="code").
To disable this, explicitly set "adaptive_caching": false and/or "run_with": null in your definition.
By default, workflows created via MCP use Code 2.0 (code_version=2, run_with="code").
To disable this, explicitly set "code_version": 1 and/or "run_with": null in your definition.
Best practice: use one block per logical step with a short focused prompt (2-3 sentences).
Use "navigation" blocks for actions (filling forms, clicking) and "extraction" blocks for pulling data.

View file

@ -1939,6 +1939,7 @@ class AgentDB(BaseAlchemyDB):
ai_fallback: bool = True,
cache_key: str | None = None,
adaptive_caching: bool = False,
code_version: int | None = None,
generate_script_on_terminal: bool = False,
run_sequentially: bool = False,
sequential_key: str | None = None,
@ -1964,6 +1965,7 @@ class AgentDB(BaseAlchemyDB):
ai_fallback=ai_fallback,
cache_key=cache_key or DEFAULT_SCRIPT_RUN_ID,
adaptive_caching=adaptive_caching,
code_version=code_version,
generate_script_on_terminal=generate_script_on_terminal,
run_sequentially=run_sequentially,
sequential_key=sequential_key,
@ -7464,7 +7466,7 @@ class AgentDB(BaseAlchemyDB):
base_filters = [
WorkflowRunModel.workflow_run_id.in_(run_ids_subquery),
WorkflowRunModel.organization_id == organization_id,
WorkflowRunModel.run_with.in_(["code", "code_v2"]),
WorkflowRunModel.run_with.in_(["code", "code_v2"]), # include legacy "code_v2" rows
]
# Count statuses via GROUP BY (also gives us total_count)
@ -7550,7 +7552,7 @@ class AgentDB(BaseAlchemyDB):
WorkflowScriptModel.deleted_at.is_(None),
WorkflowScriptModel.workflow_run_id.isnot(None),
WorkflowRunModel.organization_id == organization_id,
WorkflowRunModel.run_with.in_(["code", "code_v2"]),
WorkflowRunModel.run_with.in_(["code", "code_v2"]), # include legacy "code_v2" rows
)
.group_by(WorkflowScriptModel.script_id, WorkflowRunModel.status)
)

View file

@ -307,6 +307,7 @@ class WorkflowModel(Base):
ai_fallback = Column(Boolean, default=True, nullable=False, server_default=sqlalchemy.true())
cache_key = Column(String, nullable=True)
adaptive_caching = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.false())
code_version = Column(Integer, nullable=True, server_default=sqlalchemy.text("2"))
generate_script_on_terminal = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.false())
run_sequentially = Column(Boolean, nullable=True)
sequential_key = Column(String, nullable=True)

View file

@ -401,6 +401,7 @@ def convert_to_workflow(
ai_fallback=workflow_model.ai_fallback,
cache_key=workflow_model.cache_key,
adaptive_caching=workflow_model.adaptive_caching,
code_version=workflow_model.code_version,
generate_script_on_terminal=workflow_model.generate_script_on_terminal,
run_sequentially=workflow_model.run_sequentially,
sequential_key=workflow_model.sequential_key,

View file

@ -713,7 +713,7 @@ async def get_workflow_script_blocks(
if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found")
include_main_script = bool(workflow.adaptive_caching)
include_main_script = True
workflow_run_id = block_script_request.workflow_run_id
if workflow_run_id:
workflow_run = await app.DATABASE.get_workflow_run(

View file

@ -106,6 +106,7 @@ class Workflow(BaseModel):
ai_fallback: bool = True
cache_key: str | None = None
adaptive_caching: bool = False
code_version: int | None = None
generate_script_on_terminal: bool = False
run_sequentially: bool | None = None
sequential_key: str | None = None
@ -188,11 +189,25 @@ class WorkflowRun(BaseModel):
def is_adaptive_caching(workflow: Workflow, workflow_run: WorkflowRun) -> bool:
"""Compute effective adaptive caching mode from run-level override or workflow setting."""
if workflow_run.run_with == "code_v2":
return True
if workflow_run.run_with in ("code", "agent"):
"""Compute effective adaptive caching mode from run-level override or workflow setting.
Uses code_version >= 2 as the primary check. Falls back to the legacy
adaptive_caching bool for rows that haven't been backfilled yet
(code_version is None).
"""
run_with = workflow_run.run_with or workflow.run_with
# Explicit agent mode → never adaptive
if run_with == "agent":
return False
# When run_with is "code" (or legacy "code_v2"), check code_version
if run_with in ("code", "code_v2"):
if workflow.code_version is not None:
return workflow.code_version >= 2
return workflow.adaptive_caching
# run_with is None — check code_version as the implicit fallback
# (workflows with code_version >= 2 implicitly run as code)
if workflow.code_version is not None:
return workflow.code_version >= 2
return workflow.adaptive_caching

View file

@ -615,16 +615,19 @@ class WorkflowService:
if workflow_request.extra_http_headers is None and workflow.extra_http_headers is not None:
workflow_request.extra_http_headers = workflow.extra_http_headers
# Force ai_fallback=True for adaptive caching (code_v2) runs.
# Force ai_fallback=True for adaptive caching (code_version >= 2) runs.
# Adaptive caching requires AI fallback to self-heal when cached scripts break.
# Without this, a caller sending ai_fallback=false would silently disable recovery.
if workflow_request.run_with == "code_v2" or (workflow_request.run_with is None and workflow.adaptive_caching):
effective_code_version = (
workflow.code_version if workflow.code_version is not None else (2 if workflow.adaptive_caching else None)
)
if (effective_code_version or 0) >= 2 and (workflow_request.run_with in ("code", None)):
if workflow_request.ai_fallback is False:
LOG.info(
"Overriding ai_fallback to True for adaptive caching run",
workflow_permanent_id=workflow_permanent_id,
request_run_with=workflow_request.run_with,
workflow_adaptive_caching=workflow.adaptive_caching,
workflow_code_version=workflow.code_version,
)
workflow_request.ai_fallback = True
@ -1307,7 +1310,7 @@ class WorkflowService:
LOG.error("Failed to load static script", exc_info=True)
# Mark workflow as running, preserving the user's original run_with intent.
# The run_with field records what the user requested (e.g. "code_v2"),
# The run_with field records what the user requested (e.g. "code"),
# not whether a script was actually found. Execution mode is determined
# separately by is_script_run and script_mode below.
await self.mark_workflow_run_as_running(workflow_run_id=workflow_run_id, run_with=workflow_run.run_with)
@ -2209,7 +2212,7 @@ class WorkflowService:
and block.label not in script_blocks_by_label
and workflow_run_block_result.status in cacheable_statuses
and block.block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
# For traditional caching (adaptive_caching=False), only track blocks
# For traditional caching (code_version < 2), only track blocks
# for regeneration when actually running with code. Agent-mode runs
# should not trigger regeneration — doing so creates an infinite loop
# where every run deletes and regenerates the script because blocks
@ -2687,6 +2690,7 @@ class WorkflowService:
sequential_key: str | None = None,
folder_id: str | None = None,
adaptive_caching: bool = False,
code_version: int | None = None,
generate_script_on_terminal: bool = False,
) -> Workflow:
try:
@ -2714,6 +2718,7 @@ class WorkflowService:
sequential_key=sequential_key,
folder_id=folder_id,
adaptive_caching=adaptive_caching,
code_version=code_version,
generate_script_on_terminal=generate_script_on_terminal,
)
except IntegrityError as e:
@ -4403,6 +4408,7 @@ class WorkflowService:
sequential_key=request.sequential_key,
folder_id=existing_latest_workflow.folder_id,
adaptive_caching=request.adaptive_caching,
code_version=request.code_version,
generate_script_on_terminal=request.generate_script_on_terminal,
)
else:
@ -4429,6 +4435,7 @@ class WorkflowService:
sequential_key=request.sequential_key,
folder_id=request.folder_id,
adaptive_caching=request.adaptive_caching,
code_version=request.code_version,
generate_script_on_terminal=request.generate_script_on_terminal,
)
# Keeping track of the new workflow id to delete it if an error occurs during the creation process
@ -5200,20 +5207,22 @@ class WorkflowService:
) -> bool:
"""Determine whether this run should attempt to execute cached scripts.
Priority: run-level run_with > workflow-level run_with > adaptive_caching fallback > default (agent).
When adaptive_caching is enabled at the workflow level and no explicit run_with
is set, the run defaults to code mode so cached scripts are actually used.
Priority: run-level run_with > workflow-level run_with > code_version fallback > default (agent).
When code_version >= 1 at the workflow level and no explicit run_with is set,
the run defaults to code mode so cached scripts are actually used.
"""
if workflow_run.run_with in ("code", "code_v2"):
return True
if workflow_run.run_with == "agent":
return False
if workflow.run_with in ("code", "code_v2"):
if workflow.run_with in ("code", "code_v2"): # include legacy "code_v2" workflows
return True
if workflow.run_with == "agent":
return False
# No explicit run_with on either workflow or run — fall back to
# adaptive_caching: if the workflow has caching enabled, run code.
# code_version / adaptive_caching: if the workflow has caching enabled, run code.
if workflow.code_version is not None:
return workflow.code_version >= 1
if workflow.adaptive_caching:
return True
return False

View file

@ -521,10 +521,18 @@ class WorkflowRunRequest(BaseModel):
)
run_with: str | None = Field(
default=None,
description="Whether to run the workflow with agent, code, or code_v2 (adaptive caching).",
examples=["agent", "code", "code_v2"],
description="Whether to run the workflow with agent or code.",
examples=["agent", "code"],
)
@field_validator("run_with", mode="before")
@classmethod
def normalize_run_with(cls, v: str | None) -> str | None:
"""Normalize legacy 'code_v2' to 'code'."""
if v == "code_v2":
return "code"
return v
@field_validator("webhook_url", "totp_url")
@classmethod
def validate_urls(cls, url: str | None) -> str | None:
@ -634,9 +642,18 @@ class WorkflowRunResponse(BaseRunResponse):
run_type: Literal[RunType.workflow_run] = Field(description="Type of run - always workflow_run for workflow runs")
run_with: str | None = Field(
default=None,
description="Whether the workflow run was executed with agent, code, or code_v2 (adaptive caching)",
examples=["agent", "code", "code_v2"],
description="Whether the workflow run was executed with agent or code",
examples=["agent", "code"],
)
@field_validator("run_with", mode="before")
@classmethod
def normalize_run_with(cls, v: str | None) -> str | None:
"""Normalize legacy 'code_v2' to 'code' in API responses."""
if v == "code_v2":
return "code"
return v
ai_fallback: bool | None = Field(
default=None,
description="Whether to fallback to AI if code run fails.",

View file

@ -1078,6 +1078,7 @@ class WorkflowCreateYAMLRequest(BaseModel):
ai_fallback: bool = True
cache_key: str | None = "default"
adaptive_caching: bool = False
code_version: int | None = Field(default=None, ge=1, le=2)
generate_script_on_terminal: bool = False
run_sequentially: bool = False
sequential_key: str | None = None

View file

@ -34,11 +34,11 @@ def _minimal_workflow_json(**overrides: object) -> str:
def test_defaults_injected_when_not_specified() -> None:
"""When adaptive_caching and run_with are omitted, _inject_code_v2_defaults adds them."""
"""When code_version and run_with are omitted, _inject_code_v2_defaults adds them."""
definition = _minimal_workflow_json()
result = _inject_code_v2_defaults(definition, "json")
parsed = json.loads(result)
assert parsed["adaptive_caching"] is True
assert parsed["code_version"] == 2
assert parsed["run_with"] == "code"
@ -47,16 +47,16 @@ def test_defaults_injected_in_auto_mode() -> None:
definition = _minimal_workflow_json()
result = _inject_code_v2_defaults(definition, "auto")
parsed = json.loads(result)
assert parsed["adaptive_caching"] is True
assert parsed["code_version"] == 2
assert parsed["run_with"] == "code"
def test_explicit_values_preserved() -> None:
"""When the user explicitly sets these fields, their values are preserved."""
definition = _minimal_workflow_json(adaptive_caching=False, run_with="agent")
definition = _minimal_workflow_json(code_version=1, run_with="agent")
result = _inject_code_v2_defaults(definition, "json")
parsed = json.loads(result)
assert parsed["adaptive_caching"] is False
assert parsed["code_version"] == 1
assert parsed["run_with"] == "agent"
@ -66,8 +66,8 @@ def test_explicit_null_run_with_preserved() -> None:
result = _inject_code_v2_defaults(definition, "json")
parsed = json.loads(result)
assert parsed["run_with"] is None
# adaptive_caching was not set, so it gets the default
assert parsed["adaptive_caching"] is True
# code_version was not set, so it gets the default
assert parsed["code_version"] == 2
def test_proxy_default_injected_when_not_specified_json() -> None:

View file

@ -1,8 +1,8 @@
"""Tests for WorkflowService.should_run_script() and script reviewer gating.
Verifies the priority chain:
run-level run_with > workflow-level run_with > adaptive_caching fallback > default (agent).
When adaptive_caching=True and no explicit run_with is set, the run defaults to code mode.
run-level run_with > workflow-level run_with > code_version fallback > default (agent).
When code_version >= 1 and no explicit run_with is set, the run defaults to code mode.
Also verifies that the script reviewer only fires when the script was actually
executed (should_run_script=True), not merely when adaptive caching is enabled.
@ -20,7 +20,11 @@ from skyvern.forge.sdk.workflow.models.workflow import (
)
def _make_workflow(run_with: str | None = None, adaptive_caching: bool = False) -> Workflow:
def _make_workflow(
run_with: str | None = None,
adaptive_caching: bool = False,
code_version: int | None = None,
) -> Workflow:
return Workflow(
workflow_id="wf_test",
organization_id="org_test",
@ -31,6 +35,7 @@ def _make_workflow(run_with: str | None = None, adaptive_caching: bool = False)
workflow_definition={"parameters": [], "blocks": []},
run_with=run_with,
adaptive_caching=adaptive_caching,
code_version=code_version,
created_at=datetime.now(timezone.utc),
modified_at=datetime.now(timezone.utc),
)
@ -57,26 +62,21 @@ def service():
class TestShouldRunScript:
"""Run-level run_with takes priority, then workflow-level, then adaptive_caching fallback, then agent."""
"""Run-level run_with takes priority, then workflow-level, then code_version fallback, then agent."""
def test_run_code_overrides_workflow_agent(self, service):
wf = _make_workflow(run_with="agent")
wr = _make_run(run_with="code")
assert service.should_run_script(wf, wr) is True
def test_run_code_v2_overrides_workflow_agent(self, service):
wf = _make_workflow(run_with="agent")
wr = _make_run(run_with="code_v2")
assert service.should_run_script(wf, wr) is True
def test_run_agent_overrides_workflow_code(self, service):
wf = _make_workflow(run_with="code")
wr = _make_run(run_with="agent")
assert service.should_run_script(wf, wr) is False
def test_run_agent_overrides_adaptive_caching(self, service):
"""Explicit run-level agent overrides adaptive_caching fallback."""
wf = _make_workflow(run_with=None, adaptive_caching=True)
def test_run_agent_overrides_code_version(self, service):
"""Explicit run-level agent overrides code_version fallback."""
wf = _make_workflow(run_with=None, code_version=2)
wr = _make_run(run_with="agent")
assert service.should_run_script(wf, wr) is False
@ -90,36 +90,42 @@ class TestShouldRunScript:
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is False
def test_workflow_agent_overrides_adaptive_caching(self, service):
"""Explicit workflow-level agent takes priority over adaptive_caching."""
wf = _make_workflow(run_with="agent", adaptive_caching=True)
def test_workflow_agent_overrides_code_version(self, service):
"""Explicit workflow-level agent takes priority over code_version."""
wf = _make_workflow(run_with="agent", code_version=2)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is False
def test_both_null_defaults_to_agent(self, service):
"""When neither workflow nor run specifies run_with and no adaptive_caching, default to agent."""
"""When neither workflow nor run specifies run_with and no code_version, default to agent."""
wf = _make_workflow(run_with=None)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is False
def test_adaptive_caching_defaults_to_code(self, service):
"""adaptive_caching=True with run_with=null should default to code mode."""
def test_code_version_defaults_to_code(self, service):
"""code_version >= 1 with run_with=null should default to code mode."""
wf = _make_workflow(run_with=None, code_version=2)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
def test_code_version_1_defaults_to_code(self, service):
"""code_version=1 with run_with=null should also run code."""
wf = _make_workflow(run_with=None, code_version=1)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
def test_code_version_with_workflow_code_runs_code(self, service):
"""code_version=2 with run_with=code should run code."""
wf = _make_workflow(run_with="code", code_version=2)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
def test_legacy_adaptive_caching_fallback(self, service):
"""Legacy adaptive_caching=True with no code_version should still default to code."""
wf = _make_workflow(run_with=None, adaptive_caching=True)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
def test_adaptive_caching_with_workflow_code_runs_code(self, service):
"""adaptive_caching=True with run_with=code should run code (v1→v2 upgrade is separate)."""
wf = _make_workflow(run_with="code", adaptive_caching=True)
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
def test_workflow_code_v2_with_null_run(self, service):
"""workflow.run_with='code_v2' should be treated the same as 'code'."""
wf = _make_workflow(run_with="code_v2")
wr = _make_run(run_with=None)
assert service.should_run_script(wf, wr) is True
class TestScriptReviewerGate:
"""The script reviewer should only fire when the script was actually executed.
@ -129,20 +135,26 @@ class TestScriptReviewerGate:
"""
@pytest.mark.parametrize(
"run_with,adaptive_caching,expect_reviewer",
"run_with,code_version,expect_reviewer",
[
("code_v2", True, True),
("code_v2", False, True), # is_adaptive_caching=True for code_v2 regardless
(None, True, True), # adaptive_caching=True defaults to code mode now
(None, False, False),
("agent", True, False),
("agent", False, False),
("code", True, False), # code without code_v2 → is_adaptive_caching=False
("code", False, False),
("code", 2, True), # code + code_version=2 → adaptive caching on
("code", 1, False), # code + code_version=1 → adaptive caching off
(None, 2, True), # code_version=2 defaults to code mode, adaptive
(None, None, False), # no code_version, no run_with → agent
("agent", 2, False), # agent overrides everything
("agent", None, False),
("code", None, False), # code without code_version → not adaptive
],
)
def test_reviewer_gate(self, service, run_with, adaptive_caching, expect_reviewer):
wf = _make_workflow(run_with=None, adaptive_caching=adaptive_caching)
def test_reviewer_gate(self, service, run_with, code_version, expect_reviewer):
wf = _make_workflow(run_with=None, code_version=code_version)
wr = _make_run(run_with=run_with)
should_review = is_adaptive_caching(wf, wr) and service.should_run_script(wf, wr)
assert should_review is expect_reviewer
def test_legacy_adaptive_caching_backward_compat(self, service):
"""Legacy adaptive_caching=True with code_version=None still enables reviewer."""
wf = _make_workflow(run_with=None, adaptive_caching=True, code_version=None)
wr = _make_run(run_with="code")
should_review = is_adaptive_caching(wf, wr) and service.should_run_script(wf, wr)
assert should_review is True