diff --git a/skyvern/core/script_generations/CLAUDE.md b/skyvern/core/script_generations/CLAUDE.md new file mode 100644 index 00000000..af676145 --- /dev/null +++ b/skyvern/core/script_generations/CLAUDE.md @@ -0,0 +1,32 @@ +# Script Generation Context + +## Key Constants + +- `SCRIPT_TASK_BLOCKS` - Block types that have task_id and actions (task, navigation, extraction, etc.) +- `BLOCK_TYPES_THAT_SHOULD_BE_CACHED` in `workflow/service.py` - Block types eligible for caching (includes for_loop) + +## Script Block Requirements for `run_with: code` + +For a workflow to execute with cached scripts (`run_with: code`), ALL top-level blocks must have: +1. A `script_block` database entry +2. A non-null `run_signature` field + +Without these, the system falls back to `run_with: agent`. + +## Adding New Cacheable Block Types + +When adding a new block type that should support cached execution: +1. Add to `BLOCK_TYPES_THAT_SHOULD_BE_CACHED` in `workflow/service.py` +2. Add handling in `generate_workflow_script_python_code()` with BOTH: + - `create_or_update_script_block()` - stores metadata in database + - `append_block_code(block_code)` - adds code to generated script output +3. Ensure `run_signature` is set (the code statement to execute the block) + +**CRITICAL**: Every block type needs BOTH database entry AND script output. Missing `append_block_code()` causes runtime failures even if database entries exist. + +## Block Processing Order in generate_script.py + +1. `task_v1_blocks` - Blocks in `SCRIPT_TASK_BLOCKS` +2. `task_v2_blocks` - task_v2 blocks with child blocks +3. `for_loop_blocks` - ForLoop container blocks +4. `__start_block__` - Workflow entry point diff --git a/skyvern/core/script_generations/generate_script.py b/skyvern/core/script_generations/generate_script.py index ee361fc5..d4c3ce25 100644 --- a/skyvern/core/script_generations/generate_script.py +++ b/skyvern/core/script_generations/generate_script.py @@ -2305,6 +2305,50 @@ async def generate_workflow_script_python_code( append_block_code(block_code) + # Handle for_loop blocks + # ForLoop blocks need script_block entries with run_signature so they can be executed via cached scripts + for_loop_blocks = [block for block in blocks if block["block_type"] == "for_loop"] + for for_loop_block in for_loop_blocks: + for_loop_label = for_loop_block.get("label") or f"for_loop_{for_loop_block.get('workflow_run_block_id')}" + + cached_source = cached_blocks.get(for_loop_label) + use_cached = cached_source is not None and for_loop_label not in updated_block_labels + + block_workflow_run_id = for_loop_block.get("workflow_run_id") or run_id + block_workflow_run_block_id = for_loop_block.get("workflow_run_block_id") + + if use_cached: + assert cached_source is not None + block_code = cached_source.code + run_signature = cached_source.run_signature + block_workflow_run_id = cached_source.workflow_run_id + block_workflow_run_block_id = cached_source.workflow_run_block_id + else: + # Build the for loop statement + for_loop_stmt = _build_for_loop_statement(for_loop_label, for_loop_block) + temp_module = cst.Module(body=[for_loop_stmt]) + block_code = temp_module.code + run_signature = block_code.strip() + + if script_id and script_revision_id and organization_id: + try: + await create_or_update_script_block( + block_code=block_code, + script_revision_id=script_revision_id, + script_id=script_id, + organization_id=organization_id, + block_label=for_loop_label, + update=pending, + run_signature=run_signature, + workflow_run_id=block_workflow_run_id, + workflow_run_block_id=block_workflow_run_block_id, + input_fields=None, + ) + except Exception as e: + LOG.error("Failed to create for_loop script block", error=str(e), exc_info=True) + + append_block_code(block_code) + # --- runner --------------------------------------------------------- run_fn = _build_run_fn(blocks, workflow_run_request)