Create a sequence of tasks with a task graph

In Snowflake, you can manage multiple tasks with a task graph, also known as a directed acyclic graph, or DAG. A task graph is composed of a root task and dependent child tasks. The dependencies must run in a start-to-finish direction, with no loops. An optional final task (finalizer) can perform cleanup operations after all other tasks are complete.

Build task graphs that have dynamic behavior by specifying logic-based operations in the task body using runtime values, graph level configuration, and return values of parent tasks.

You can create tasks and task graphs using supported languages and tools like SQL, JavaScript, Python, Java, Scala, or Snowflake Scripting. This topic provides SQL examples. For Python examples, see Managing Snowflake tasks and task graphs with Python.

Create a task graph

Create a root task using CREATE TASK, then create child tasks using CREATE TASK .. AFTER to select the parent tasks.

The root task defines when the task graph runs. Child tasks are executed in the order defined by the task graph.

When multiple child tasks have the same parent, the child tasks run in parallel.

When a task has multiple parents, the task waits for all preceding tasks to successfully complete before starting. (The task may also run when some parent tasks are skipped. For more information, see Skip or suspend a child task).

The following example creates a serverless task graph that starts with a root task that is scheduled to run every minute. The root task has two child tasks that run in parallel. (The diagram shows an example where one of these tasks runs longer than the other.) After both of those tasks complete, a third child task runs. The finalizer task runs after all other tasks complete or fail to complete:

A diagram of a sequence of tasks.
CREATE TASK "task_root"
  SCHEDULE = '1 MINUTE'
  AS SELECT 1;

CREATE TASK "task_a"
  AFTER "task_root"
  AS SELECT 1;

CREATE TASK "task_b"
  AFTER "task_root"
  AS SELECT 1;

CREATE TASK "task_c"
  AFTER "task_a", "task_b"
  AS SELECT 1;
Copy

Considerations:

  • A task graph is limited to a maximum of 1000 tasks.

  • A single task can have a maximum of 100 parent tasks and 100 child tasks.

  • When tasks run in parallel on the same user-managed warehouse, the compute resources must be sized to handle the concurrent task runs.

Finalizer task

You can add an optional finalizer task to run after all other tasks in the task graph complete (or fail to complete). Use this to do the following:

  • Perform cleanup operations, for example, cleaning up intermediate data that is no longer needed.

  • Send notifications about task success or failure.

A task sequence shows a root task pointing to two child tasks, which in turn point to another task. A finalizer task is shown at the bottom, running after all other tasks complete or fail to complete.

To create a finalizer task, use CREATE TASK … FINALIZE … on the root task. Example:

CREATE TASK "task_finalizer"
  FINALIZE = "task_root"
  AS SELECT 1;
Copy

Considerations:

  • A finalizer task is always associated with a root task. Each root task can have only one finalizer task, and a finalizer task can be associated with only one root task.

  • When the root task of a task graph is skipped (for example, because of overlapping task graph runs), the finalizer task won’t be started.

  • A finalizer task cannot have any child tasks.

  • A finalizer task is scheduled only when no other tasks are running or queued in the current task graph run.

For more examples, see Finalizer task example: Send email notification and Finalizer task example: Correct for errors.

Manage task graph ownership

All tasks in a task graph must have the same task owner and be stored in the same database and schema.

You can transfer ownership of all tasks in a task graph using one of the following actions:

  • Drop the owner of all tasks in the task graph using DROP ROLE. Snowflake transfers ownership to the role that runs the DROP ROLE command.

  • Transfer ownership of all tasks in the task graph using GRANT OWNERSHIP on all tasks in a schema.

When you transfer ownership of the tasks in a task graph using these methods, the tasks in the task graph retain their relationships to each other.

Transferring ownership of a single task removes the dependency between the task and any parent and child tasks. For more information, see Unlink parent and child tasks (in this topic).

Note

Database replication does not work for task graphs if the graph is owned by a different role than the role that performs replication.

Run or schedule tasks in a task graph

Run a task graph manually

You can run a single instance of a task graph. This is useful for testing new or modified task graphs before enabling the task graph in production, or for one-time runs as needed.

Before starting the task graph, use ALTER TASK … RESUME on each child task (including the optional finalizer task) that you want to include in the run.

To run a single instance of a task graph, use EXECUTE TASK on the root task. When you run the root task, all resumed child tasks in the task graph are executed in the order defined by the task graph.

Run a task on a schedule or as a triggered task

In the root task, define when the task graph runs. Task graphs can run on a recurring schedule, or they can be triggered by an event. For more information, see the following topics:

To start the task graph, you can do either of the following:

  • Resume each individual child task (including the finalizer) that you want to include in the run, and then resume the root task, using ALTER TASK … RESUME.

  • Resume all of the tasks in a task graph at once using SYSTEM$TASK_DEPENDENTS_ENABLE ( <root_task_name> ) on the root task.

View dependent tasks in a task graph

To view the child tasks for a root task, call the TASK_DEPENDENTS table function. To retrieve all tasks in a task graph, input the root task when calling the function.

You can also use Snowsight to manage and view your task graphs. For more information, see Viewing tasks and task graphs in Snowsight.

Modify, suspend, or retry tasks

Modify a task in a task graph

To modify a task in a scheduled task graph, suspend the root task using ALTER TASK … SUSPEND. If a run of the task graph is in process, it completes the current run. All future scheduled runs of the root task are canceled.

When the root task is suspended, child tasks including the finalizer task retain their state (suspended, running, or completed). The child tasks don’t need to be individually suspended.

After you suspend the root task, you can modify any task in the task graph.

To resume the task graph, you can do either of the following:

  • Resume the root task using ALTER TASK … RESUME. Individual child tasks that were running before do not need to be resumed.

  • Resume all of the tasks in a task graph at once by calling SYSTEM$TASK_DEPENDENTS_ENABLE and passing in the name of the root task.

Skip or suspend a child task

To skip a child task in a task graph, suspend the child task using ALTER TASK … SUSPEND.

When you suspend a child task, the task graph continues to run as though the child task had succeeded. A child task with multiple predecessors runs as long as at least one of the predecessors is in a resumed state, and all resumed predecessors run successfully to completion.

A diagram shows a task graph that includes a suspended child task. The suspended child task is skipped, and the task graph completes.

Retry a failed task

Use EXECUTE TASK … RETRY LAST to attempt to run the task graph from the last failed task. If the task succeeds, all child tasks will continue to run as their preceding tasks complete.

Automatic retries

By default, if a child task fails, the entire task graph is considered to have failed.

Rather than waiting until the next scheduled task graph run, you can instruct the task graph to retry immediately by setting the TASK_AUTO_RETRY_ATTEMPTS parameter on the root task. When a child task fails, the entire task graph is immediately retried, up to the number of times specified. If the task graph still does not complete, the task graph is considered to have failed.

Suspend task graphs after failed task graph runs

By default, a task graph is suspended after 10 consecutive failures. You can change this value by setting SUSPEND_TASK_AFTER_NUM_FAILURES on the root task.

In the following example, whenever a child task fails, the task graph immediately retries twice before the entire task graph is considered failed. If the task graph fails three times in a row, the task graph is then suspended.

CREATE OR REPLACE TASK "task_root"
  SCHEDULE = '1 MINUTE'
  TASK_AUTO_RETRY_ATTEMPTS = 2   --  Failed task graph retries up to 2 times
  SUSPEND_TASK_AFTER_NUM_FAILURES = 3   --  Task graph suspends after 3 consecutive failures
  AS SELECT 1;
Copy

Overlapping task graph runs

By default, Snowflake ensures that only one instance of a particular task graph is allowed to run at a time. The next run of a root task is scheduled only after all tasks in the task graph have finished running. This means that if the cumulative time required to run all tasks in the task graph exceeds the explicit scheduled time set in the definition of the root task, at least one run of the task graph is skipped.

To allow child tasks to overlap, use CREATE TASK or ALTER TASK on the root task, and set ALLOW_OVERLAPPING_EXECUTION to TRUE. (Root tasks never overlap.)

Overlapping task graph runs

Overlapping runs may be tolerated (or even desirable) when read/write SQL operations executed by overlapping runs of a task graph do not produce incorrect or duplicate data. However, for other task graphs, task owners (the role with the OWNERSHIP privilege on all tasks in the task graph) should set an appropriate schedule on the root task and choose an appropriate warehouse size (or use serverless compute resources) to ensure an instance of the task graph finishes to completion before the root task is next scheduled to run.

To better align a task graph with the schedule defined in the root task:

  1. If feasible, increase the scheduling time between runs of the root task.

  2. Consider modifying compute-heavy tasks to use serverless compute resources. If the task relies on user-managed compute resources, increase the size of the warehouse that runs large or complex SQL statements or stored procedures in the task graph.

  3. Analyze the SQL statements or stored procedure executed by each task. Determine if code can be rewritten to leverage parallel processing.

If none of the above solutions help, consider whether it is necessary to allow concurrent runs of the task graph by setting ALLOW_OVERLAPPING_EXECUTION = TRUE on the root task. This parameter can be defined when creating a task (using CREATE TASK) or later (using ALTER TASK or in Snowsight).

Versioning

When the root task in a task graph is resumed or manually executed, Snowflake sets a version of the entire task graph, including all properties for all tasks in the task graph. After a task is suspended and modified, Snowflake set a new version when the root task is resumed or manually executed.

To modify or recreate any task in a task graph, the root task must first be suspended. When the root task is suspended, all future scheduled runs of the root task are cancelled; however, if any tasks are currently running, these tasks and any descendant tasks continue to run using the current version.

Note

If the definition of a stored procedure called by a task changes while the task graph is executing, the new programming could be executed when the stored procedure is called by the task in the current run.

For example, suppose the root task in a task graph is suspended, but a scheduled run of this task has already started. The owner of all tasks in the task graph modifies the SQL code called by a child task while the root task is still running. The child task runs and executes the SQL code in its definition using the version of the task graph that was current when the root task started its run. When the root task is resumed or is manually executed, a new version of the task graph is set. This new version includes the modifications to the child task.

To retrieve the history of task versions, query TASK_VERSIONS Account Usage view (in the SNOWFLAKE shared database).

Create a task graph with logic (runtime info, configuration, and return values)

Tasks in a task graph can use return values from parent tasks to perform logic-based operations in their function body.

Get and use runtime information

Use the function SYSTEM$TASK_RUNTIME_INFO to report information about the current task run. This function has several options specific to task graphs. For example, use CURRENT_ROOT_TASK_NAME to get the name of the root task in the current task graph. The following examples shows how to add a date stamp to a table based on when the root task of the task graph started.

-- Updates the date/time table after the root task completes.
CREATE OR REPLACE TASK "task_date_time_table"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER task_root
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$TASK_RUNTIME_INFO('CURRENT_TASK_GRAPH_ORIGINAL_SCHEDULED_TIMESTAMP'));
      INSERT INTO date_time_table VALUES('order_date',:value);
    END;
Copy

Pass configuration information to the task graph

You can pass configuration information using a JSON object that can be read by other tasks in a task graph. Use the syntax CREATE/ALTER TASK … CONFIG to set, unset, or modify the configuration information in the root task. Use the function SYSTEM$GET_TASK_GRAPH_CONFIG to retrieve it. Example:

CREATE OR REPLACE TASK "task_root"
  SCHEDULE = '1 MINUTE'
  USER_TASK_TIMEOUT_MS = 60000
  CONFIG='{"environment": "production", "path": "/prod_directory/"}'
  AS
    SELECT 1;

CREATE OR REPLACE TASK "task_a"
  USER_TASK_TIMEOUT_MS = 600000
  AFTER task_root
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$GET_TASK_GRAPH_CONFIG('path'));
      CREATE TABLE IF NOT EXISTS demo_table(NAME VARCHAR, VALUE VARCHAR);
      INSERT INTO demo_table VALUES('task c path',:value);
    END;
Copy

Pass return values between tasks

You can pass return values between tasks in a task graph. Use the function SYSTEM$SET_RETURN_VALUE to add a return value from a task, and use the function SYSTEM$GET_PREDECESSOR_RETURN_VALUE to retrieve it.

When a task has multiple predecessors, you must specify which task has the return value that you want. In the following example, we create a root task in a task graph that adds configuration information.

CREATE OR REPLACE TASK "task_c"
  SCHEDULE = '1 MINUTE'
  USER_TASK_TIMEOUT_MS = 60000
  AS
    BEGIN
      CALL SYSTEM$SET_RETURN_VALUE('task_c successful');
    END;

CREATE OR REPLACE TASK "task_d"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER task_c
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$GET_PREDECESSOR_RETURN_VALUE('task_c'));
      CREATE TABLE IF NOT EXISTS demo_table(NAME VARCHAR, VALUE VARCHAR);
      INSERT INTO demo_table VALUES('Value from predecessor task_c', :value);
    END;
Copy

Examples

Example: Start multiple tasks and report status

In the following example, the root task starts tasks to update three different tables. After those three tables are updated, a task combines the information from the other three tables into an aggregate sales table.

Flowchart shows a root task that starts three child tasks, each of which updates a table. Those three tasks all precede another child task, which combines the previous changes into another table.
-- Create a notebook in the public schema
-- USE DATABASE <database name>;
-- USE SCHEMA <schema name>;

-- task_a: Root task. Starts the task graph and sets basic configurations.
CREATE OR REPLACE TASK "task_a"
  SCHEDULE = '1 MINUTE'
  TASK_AUTO_RETRY_ATTEMPTS = 2
  SUSPEND_TASK_AFTER_NUM_FAILURES = 3
  USER_TASK_TIMEOUT_MS = 60000
  CONFIG='{"environment": "production", "path": "/prod_directory/"}'
  AS
    BEGIN
      CALL SYSTEM$SET_RETURN_VALUE('task_a successful');
    END;
;

-- task_customer_table: Updates the customer table.
--   Runs after the root task completes.
CREATE OR REPLACE TASK "task_customer_table"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER "task_a"
  AS
    BEGIN
      LET VALUE := (SELECT customer_id FROM ref_cust_table
        WHERE cust_name = "Jane Doe";);
      INSERT INTO customer_table VALUES('customer_id',:value);
    END;
;

-- task_product_table: Updates the product table.
--   Runs after the root task completes.
CREATE OR REPLACE TASK "task_product_table"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER "task_a"
  AS
    BEGIN
      LET VALUE := (SELECT product_id FROM ref_item_table
        WHERE PRODUCT_NAME = "widget";);
      INSERT INTO product_table VALUES('product_id',:value);
    END;
;

-- task_date_time_table: Updates the date/time table.
--   Runs after the root task completes.
CREATE OR REPLACE TASK "task_date_time_table"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER "task_a"
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$TASK_RUNTIME_INFO('CURRENT_TASK_GRAPH_ORIGINAL_SCHEDULED_TIMESTAMP'));
      INSERT INTO "date_time_table" VALUES('order_date',:value);
    END;
;

-- task_sales_table: Aggregates changes from other tables.
--   Runs only after updates are complete to all three other tables.
CREATE OR REPLACE TASK "task_sales_table"
  USER_TASK_TIMEOUT_MS = 60
  AFTER task_customer_table, task_product_table, task_date_time_table
  AS
    BEGIN
      LET VALUE := (SELECT sales_order_id FROM ORDERS);
      JOIN CUSTOMER_TABLE ON orders.customer_id=customer_table.customer_id;
      INSERT INTO "sales_table" VALUES('sales_order_id',:value);
    END;
;
Copy

Finalizer task example: Send email notification

This example shows how a finalizer task can send email that summarizes how the task graph performed. The task calls two external functions: one aggregates information about task completion status, and the other uses the info to compose an email that can be sent through a remote messaging service.

A task sequence shows a root task pointing to two child tasks, which in turn point to another task. A finalizer task is shown at the bottom, running after all other tasks finish or fail to finish.
CREATE OR REPLACE TASK "notify_finalizer"
  USER_TASK_TIMEOUT_MS = 60
  FINALIZE = task_root
AS
  DECLARE
    my_root_task_id STRING;
    my_start_time TIMESTAMP_LTZ;
    summary_json STRING;
    summary_html STRING;
  BEGIN
    --- Get root task ID
    my_root_task_id := (CALL SYSTEM$TASK_RUNTIME_INFO('CURRENT_ROOT_TASK_UUID'));
    --- Get root task scheduled time
    my_start_time := (CALL SYSTEM$TASK_RUNTIME_INFO('CURRENT_TASK_GRAPH_ORIGINAL_SCHEDULED_TIMESTAMP'));
    --- Combine all task run info into one JSON string
    summary_json := (SELECT get_task_graph_run_summary(:my_root_task_id, :my_start_time));
    --- Convert JSON into HTML table
    summary_html := (SELECT HTML_FROM_JSON_TASK_RUNS(:summary_json));

    --- Send HTML to email
    CALL SYSTEM$SEND_EMAIL(
        'email_notification',
        'admin@snowflake.com',
        'notification task run summary',
        :summary_html,
        'text/html');
    --- Set return value for finalizer
    CALL SYSTEM$SET_RETURN_VALUE('✅ Graph run summary sent.');
  END

CREATE OR REPLACE FUNCTION get_task_graph_run_summary(my_root_task_id STRING, my_start_time TIMESTAMP_LTZ)
  RETURNS STRING
AS
$$
  (SELECT
    ARRAY_AGG(OBJECT_CONSTRUCT(
      'task_name', name,
      'run_status', state,
      'return_value', return_value,
      'started', query_start_time,
      'duration', duration,
      'error_message', error_message
      )
    ) AS GRAPH_RUN_SUMMARY
  FROM
    (SELECT
      NAME,
      CASE
        WHEN STATE = 'SUCCEED' then '🟢 Succeeded'
        WHEN STATE = 'FAILED' then '🔴 Failed'
        WHEN STATE = 'SKIPPED' then '🔵 Skipped'
        WHEN STATE = 'CANCELLED' then '🔘 Cancelled'
      END AS STATE,
      RETURN_VALUE,
      TO_VARCHAR(QUERY_START_TIME, 'YYYY-MM-DD HH24:MI:SS') AS QUERY_START_TIME,
      CONCAT(TIMESTAMPDIFF('seconds', query_start_time, completed_time),
        ' s') AS DURATION,
      ERROR_MESSAGE
    FROM
      TABLE(my-database.information_schema.task_history(
        ROOT_TASK_ID => my_root_task_id ::STRING,
        SCHEDULED_TIME_RANGE_START => my_start_time,
        SCHEDULED_TIME_RANGE_END => current_timestamp()
      ))
    ORDER BY
      SCHEDULED_TIME)
  )::STRING
$$
;

CREATE OR REPLACE FUNCTION HTML_FROM_JSON_TASK_RUNS(JSON_DATA STRING)
  RETURNS STRING
  LANGUAGE PYTHON
  RUNTIME_VERSION = '3.8'
  HANDLER = 'GENERATE_HTML_TABLE'
AS
$$
  IMPORT JSON

  def GENERATE_HTML_TABLE(JSON_DATA):
    column_widths = ["320px", "120px", "400px", "160px", "80px", "480px"]

  DATA = json.loads(JSON_DATA)
  HTML = f"""
    <img src="https://example.com/logo.jpg"
      alt="Company logo" height="72">
    <p><strong>Task Graph Run Summary</strong>
      <br>Sign in to Snowsight to see more details.</p>
    <table border="1" style="border-color:#DEE3EA"
      cellpadding="5" cellspacing="0">
      <thead>
        <tr>
        """
        headers = ["Task name", "Run status", "Return value", "Started", "Duration", "Error message"]
        for i, header in enumerate(headers):
            HTML += f'<th scope="col" style="text-align:left;
            width: {column_widths[i]}">{header.capitalize()}</th>'

        HTML +="""
        </tr>
      </thead>
      <tbody>
        """
        for ROW_DATA in DATA:
          HTML += "<tr>"
          for header in headers:
            key = header.replace(" ", "_").upper()
            CELL_DATA = ROW_DATA.get(key, "")
            HTML += f'<td style="text-align:left;
            width: {column_widths[headers.index(header)]}">{CELL_DATA}</td>'
          HTML += "</tr>"
        HTML +="""
      </tbody>
    </table>
    """
  return HTML
$$
;
Copy

Finalizer task example: Correct for errors

This example demonstrates how a finalizer task can correct for errors.

For demonstration purposes, the tasks are designed to fail during their first run. The finalizer tasks corrects the issue and restarts the tasks, which succeed on following runs:

Diagram showing a task series. Task A is shown on the upper left. An arrow points right from Task A to Task B, which points to Task C, which points to Task D. Below Task A, an arrow points to the finalizer task, Task F.
-- Configuration
-- By default, the notebook creates the objects in the public schema.
-- USE DATABASE <database name>;
-- USE SCHEMA <schema name>;

-- 1. Set the default configurations.
--    Creates a root task ("task_a"), and sets the default configurations
--    used throughout the task graph.
--    Configurations include:
--    * Each task runs after one minute, with a 60-second timeout.
--    * If a task fails, retry it twice. if it fails twice,
--      the entire task graph is considered as failed.
--    * If the task graph fails consecutively three times, suspend the task.
--    * Other environment values are set.

CREATE OR REPLACE TASK "task_a"
  SCHEDULE = '1 MINUTE'
  USER_TASK_TIMEOUT_MS = 60000
  TASK_AUTO_RETRY_ATTEMPTS = 2
  SUSPEND_TASK_AFTER_NUM_FAILURES = 3
  AS
    BEGIN
      CALL SYSTEM$SET_RETURN_VALUE('task a successful');
    END;
;

-- 2. Use a runtime reflection variable.
--    Creates a child task ("task_b").
--    By design, this example fails the first time it runs, because
--    it writes to a table ("demo_table") that doesn’t exist.
CREATE OR REPLACE TASK "task_b"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER TASK_A
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$TASK_RUNTIME_INFO('current_task_name'));
      INSERT INTO demo_table VALUES('task b name',:VALUE);
    END;
;

-- 3. Get a task graph configuration value.
--    Creates the child task ("task_c").
--    By design, this example fails the first time it runs, because
--    the predecessor task ("task_b") fails.
CREATE OR REPLACE TASK "task_c"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER task_b
  AS
    BEGIN
      CALL SYSTEM$GET_TASK_GRAPH_CONFIG('path');
      LET VALUE := (SELECT SYSTEM$GET_TASK_GRAPH_CONFIG('path'));
      INSERT INTO demo_table VALUES('task c path',:value);
    END;
;

-- 4. Get a value from a predecessor.
--    Creates the child task ("task_d").
--    By design, this example fails the first time it runs, because
--    the predecessor task ("task_c") fails.
CREATE OR REPLACE TASK "task_d"
  USER_TASK_TIMEOUT_MS = 60000
  AFTER task_c
  AS
    BEGIN
      LET VALUE := (SELECT SYSTEM$GET_PREDECESSOR_RETURN_VALUE('task_a'));
      INSERT INTO demo_table VALUES('task d: predecessor return value', :value);
    END;
;

-- 5. Create the finalizer task ("task_f"), which creates the missing demo table.
--    After the finalizer completes, the task should automatically retry
--    (see task_a: tasks_auto_retry_attempts).
--    On retry, task_b, task_c, and task_d should complete successfully.
CREATE OR REPLACE TASK "task_f"
  USER_TASK_TIMEOUT_MS = 60000
  FINALIZE = "task_a"
  AS
    BEGIN
      CREATE TABLE IF NOT EXISTS demo_table(NAME VARCHAR, VALUE VARCHAR);
    END;
;

-- 6. Resume the finalizer. Upon creation, tasks start in a suspended state.
--    Use this command to resume the finalizer and all of its child tasks.
ALTER TASK task_f RESUME;
SELECT SYSTEM$TASK_DEPENDENTS_ENABLE('task_a');

-- 7. Query the task history
SELECT
    name, state, attempt_number, scheduled_from
  FROM
    TABLE(information_schema.task_history(task_name=> 'task_b'))
  LIMIT 5;
;

-- 8. Suspend the task graph to stop incurring costs
--    Note: To stop the task graph, you only need to suspend the root task
--    (task_a). Child tasks don’t run unless the root task is run.
--    If any child tasks are running, they have a limited duration
--    and will end soon.
ALTER TASK task_a SUSPEND;
DROP TABLE demo_table;

-- 9. Check tasks during execution (optional)
--    Run this command to query the demo table during execution
--    to check which tasks have run.
SELECT * FROM demo_table;

-- 10. Demo reset (optional)
--     Run this command to remove the demo table.
--     This causes task_b to fail during its first run.
--     After the task graph retries, task_b will succeed.
DROP TABLE demo_table;
Copy