Orchestrate tasks
Orchestrate a multi-step, semi-automatic task in under 10 minutes
Airplane Tasks make it possible to orchestrate complex processes that span multiple steps and engage
with human operators. This guide will walk you through writing a task that upgrades a company's
billing plan. Along the way, we'll explore a number of important orchestration concepts like
prompts, task composition, and failure handling.
Before you begin
Before you begin
If you haven't yet, first install the Airplane CLI by following the instructions at
Installing the CLI.
If you have already installed the Airplane CLI, ensure you have the latest version by
Updating the CLI. You can check your version by running
airplane version
.Set up a database
Set up a database
If you don't already have a database set up, you can create an
Airplane Postgres database under the
resource settings page.
In this example, we will name the database
Demo DB
.On the resource settings page, you can run
this query to create a table and insert data into it:
sqlCopied1CREATE TABLE accounts (2id SERIAL PRIMARY KEY,3company_name text,4signup_date timestamp with time zone DEFAULT now(),5total_dollars integer DEFAULT 0,6country text DEFAULT 'USA'::text7);89INSERT INTO accounts (id, company_name, signup_date, total_dollars, country)10VALUES11(0, 'Future Golf Partners', '2020-03-21 04:48:23.532+00', 1655427, 'Brazil'),12(1, 'Amalgamated Star LLC', '2020-07-16 00:40:30.103+00', 43403102, 'Canada'),13(2, 'Blue Sky Corp', '2019-04-18 10:14:24.71+00', 1304097, 'France'),14(3, 'Yeet Table Inc', '2019-10-01 10:06:39.013+00', 4036934, 'Mexico');
Create a task
Create a task
We'll be developing in Studio, Airplane's development tool for building
tasks and views. For this guide, we recommend using Studio with a local development server in a new,
empty directory.
shellCopied1# mkdir airplane-getting-started-orchestration2# cd airplane-getting-started-orchestration3airplane dev
Navigate to Studio in your browser.
To create a new task, click the
New
button in the upper right.
Select
Task
to create a new task.
Choose
JavaScript
or Python
to specify a JavaScript or Python task. Name your task
Demo: Upgrade company
. You can add a description to the task if you like.
Click
Create
to create your task. This will create a few files:{slug}.airplane.ts
: Your task's entrypoint which stores configuration using inline config.package.json
: Defines your task's dependencies. Pre-configured with theAirplane JavaScript SDK
SDK.tsconfig.json
: TypeScript configuration file that enables intellisense for your task's.ts
files.
{slug}.airplane.js
: Your task's entrypoint which stores configuration using inline config.package.json
: Defines your task's dependencies. Pre-configured with theAirplane JavaScript SDK
.
{slug}_airplane.py
: Your task's entrypoint which stores configuration using inline config.requirements.txt
: Defines your task's dependencies. Pre-configured with theAirplane Python SDK
. files.
Develop locally
Develop locally
You can test your task locally by executing it in Studio. Click on the
Execute
button to run your
task locally. You'll be able to see any logs and outputs from your task in Studio.So far, our task is not doing anything interesting. Let's change that!
Query a DB
Query a DB
To upgrade a company's billing plan, we'll first need to know their ID. For this demo, we'll be
using a pre-configured PostgreSQL Resource called Demo DB.
Update your task to look up a company's ID by name by updating
{slug}.airplane.js
with the
following contents:typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query<{21id: number;22name: string;23signup_date: string;24}>(25"demo_db",26`27select28id, company_name as name, signup_date29from accounts30where31id::text ilike :query32or company_name ilike :query33order by company_name asc34`,35{ args: { query: "%" + (params.company ?? "") + "%" } },36);37const companies = run.output.Q1;3839return companies;40},41);
Update your task to look up a company's ID by name by updating
{slug}.airplane.ts
with the
following contents:javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query(21"demo_db",22`23select24id, company_name as name, signup_date25from accounts26where27id::text ilike :query28or company_name ilike :query29order by company_name asc30`,31{ args: { query: "%" + (params.company ?? "") + "%" } },32);33const companies = run.output.Q1;3435return companies;36},37);
Update your task to look up a company's ID by name by updating
{slug}_airplane.py
with the
following contents:pythonCopied1import airplane234@airplane.task(5name="Demo: Upgrade company",6# Grant this task access to the Demo DB7resources=[airplane.Resource("demo_db")],8)9def demo_upgrade_company(company: str = ""):10"""Upgrade a company's billing plan.1112Args:13company: Search query to find a company. Matches on company ID and name.14"""15run = airplane.sql.query(16"demo_db",17"""18select19id, company_name as name, signup_date20from accounts21where22id::text ilike :query23or company_name ilike :query24order by company_name asc25""",26query_args={"query": f"%{company}%"},27)28companies = run.output["Q1"]2930return companies
Our task will now perform an SQL query on the Demo DB by using the
SQL built-in. Go ahead and execute your task again to see the changes in
action.
Built-ins expose commonplace operations—such as issuing API requests and sending emails—as SDK
methods that can be accessed from tasks. To learn about the other built-ins, see
Built-ins.

When your task called the built-in SDK, it created a child run—similar to how executing a task
creates a run! This child run can be found from the
Runs
tab.
Up next, let's make this task interactive!
Prompts
Prompts
Tasks can request additional parameters at runtime using Prompts. Prompts are
dynamically created parameter forms, similar to what you see when executing a
task.
Given our list of companies, we want the operator to pick which company to upgrade. We can do this
with a prompt that has select options.
typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query<{21id: number;22name: string;23signup_date: string;24}>(25"demo_db",26`27select28id, company_name as name, signup_date29from accounts30where31id::text ilike :query32or company_name ilike :query33order by company_name asc34`,35{ args: { query: "%" + (params.company ?? "") + "%" } },36);37const companies = run.output.Q1;38if (companies.length === 0) {39throw new Error("No companies found");40}4142const { company_id } = await airplane.prompt({43company_id: {44type: "integer",45name: "Company",46options: companies.map((c) => ({ label: c.name, value: c.id })),47default: companies[0].id,48},49});5051return company_id;52},53);
javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query(21"demo_db",22`23select24id, company_name as name, signup_date25from accounts26where27id::text ilike :query28or company_name ilike :query29order by company_name asc30`,31{ args: { query: "%" + (params.company ?? "") + "%" } },32);33const companies = run.output.Q1;34if (companies.length === 0) {35throw new Error("No companies found");36}3738const { company_id } = await airplane.prompt({39company_id: {40type: "integer",41name: "Company",42options: companies.map((c) => ({ label: c.name, value: c.id })),43default: companies[0].id,44},45});4647return company_id;48},49);
pythonCopied1import airplane2# For versions of Python prior to 3.9, `Annotated` can be imported from the `typing_extensions` package.3from typing import Annotated456@airplane.task(7name="Demo: Upgrade company",8# Grant this task access to the Demo DB9resources=[airplane.Resource("demo_db")],10)11def demo_upgrade_company(company: str = ""):12"""Upgrade a company's billing plan.1314Args:15company: Search query to find a company. Matches on company ID and name.16"""17run = airplane.sql.query(18"demo_db",19"""20select21id, company_name as name, signup_date22from accounts23where24id::text ilike :query25or company_name ilike :query26order by company_name asc27""",28query_args={"query": f"%{company}%"},29)30companies = run.output["Q1"]31if len(companies) == 0:32raise Exception("No companies found")3334values = airplane.prompt({35"company_id": Annotated[int, airplane.ParamConfig(36name="Company",37options=[38airplane.LabeledOption(label=c["name"], value=c["id"]) for c in companies39],40default=companies[0]["id"],41)],42})4344return values["company_id"]
Execute this task again and you'll see that a parameter form is rendered in the run UI:

The run will wait until you pick a company before continuing.
If the company filter is specific enough that it matches only one company, we don't need to prompt
the user! Let's tweak our task to conditionally skip the prompt in that case:
typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query<{21id: number;22name: string;23signup_date: string;24}>(25"demo_db",26`27select28id, company_name as name, signup_date29from accounts30where31id::text ilike :query32or company_name ilike :query33order by company_name asc34`,35{ args: { query: "%" + (params.company ?? "") + "%" } },36);37const companies = run.output.Q1;38if (companies.length === 0) {39throw new Error("No companies found");40}4142let company_id = companies[0].id;43if (companies.length > 1) {44company_id = (45await airplane.prompt({46company_id: {47type: "integer",48name: "Company",49options: companies.map((c) => ({ label: c.name, value: c.id })),50default: company_id,51},52})53).company_id;54}5556return company_id;57},58);
javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const run = await airplane.sql.query(21"demo_db",22`23select24id, company_name as name, signup_date25from accounts26where27id::text ilike :query28or company_name ilike :query29order by company_name asc30`,31{ args: { query: "%" + (params.company ?? "") + "%" } },32);33const companies = run.output.Q1;34if (companies.length === 0) {35throw new Error("No companies found");36}3738let company_id = companies[0].id;39if (companies.length > 1) {40company_id = (41await airplane.prompt({42company_id: {43type: "integer",44name: "Company",45options: companies.map((c) => ({ label: c.name, value: c.id })),46default: company_id,47},48})49).company_id;50}5152return company_id;53},54);
pythonCopied1import airplane2# For versions of Python prior to 3.9, `Annotated` can be imported from the `typing_extensions` package.3from typing import Annotated456@airplane.task(7name="Demo: Upgrade company",8# Grant this task access to the Demo DB9resources=[airplane.Resource("demo_db")],10)11def demo_upgrade_company(company: str = ""):12"""Upgrade a company's billing plan.1314Args:15company: Search query to find a company. Matches on company ID and name.16"""17run = airplane.sql.query(18"demo_db",19"""20select21id, company_name as name, signup_date22from accounts23where24id::text ilike :query25or company_name ilike :query26order by company_name asc27""",28query_args={"query": f"%{company}%"},29)30companies = run.output["Q1"]31if not companies:32raise Exception("No companies found")3334company_id = companies[0]["id"]35if len(companies) > 1:36company_id = airplane.prompt({37"company_id": Annotated[int, airplane.ParamConfig(38name="Company",39options=[40airplane.LabeledOption(label=c["name"], value=c["id"]) for c in companies41],42default=company_id,43)]44})["company_id"]4546return company_id
If you execute this task with
"Blue Sky Corp"
, the task will now skip the prompt! If you instead
execute it with "Partner"
, this will match multiple companies, so you will still get prompted.Great! We now have a specific company to upgrade. Let's extract this logic into a separate
method to keep our main method short:
typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const companyID = await lookupCompany(params.company);2122return companyID;23},24);2526export async function lookupCompany(query: string) {27const run = await airplane.sql.query<{28id: number;29name: string;30signup_date: string;31}>(32"demo_db",33`34select35id, company_name as name, signup_date36from accounts37where38id::text ilike :query39or company_name ilike :query40order by company_name asc41`,42{ args: { query: "%" + (query ?? "") + "%" } },43);44const companies = run.output.Q1;45if (companies.length === 0) {46throw new Error("No companies found");47}4849let company_id = companies[0].id;50if (companies.length > 1) {51company_id = (52await airplane.prompt({53company_id: {54type: "integer",55name: "Company",56options: companies.map((c) => ({ label: c.name, value: c.id })),57default: company_id,58},59})60).company_id;61}6263return company_id;64}
javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17},18},19async (params) => {20const companyID = await lookupCompany(params.company);2122return companyID;23},24);2526export async function lookupCompany(query) {27const run = await airplane.sql.query(28"demo_db",29`30select31id, company_name as name, signup_date32from accounts33where34id::text ilike :query35or company_name ilike :query36order by company_name asc37`,38{ args: { query: "%" + (query ?? "") + "%" } },39);40const companies = run.output.Q1;41if (companies.length === 0) {42throw new Error("No companies found");43}4445let company_id = companies[0].id;46if (companies.length > 1) {47company_id = (48await airplane.prompt({49company_id: {50type: "integer",51name: "Company",52options: companies.map((c) => ({ label: c.name, value: c.id })),53default: company_id,54},55})56).company_id;57}5859return company_id;60}
pythonCopied1import airplane2# For versions of Python prior to 3.9, `Annotated` can be imported from the `typing_extensions` package.3from typing import Annotated456@airplane.task(7name="Demo: Upgrade company",8# Grant this task access to the Demo DB9resources=[airplane.Resource("demo_db")],10)11def demo_upgrade_company(company: str = ""):12"""Upgrade a company's billing plan.1314Args:15company: Search query to find a company. Matches on company ID and name.16"""17company_id = lookup_company(company)1819return company_id2021def lookup_company(company: str) -> int:22run = airplane.sql.query(23"demo_db",24"""25select26id, company_name as name, signup_date27from accounts28where29id::text ilike :query30or company_name ilike :query31order by company_name asc32""",33query_args={"query": f"%{company}%"},34)35companies = run.output["Q1"]36if not companies:37raise Exception("No companies found")3839company_id = companies[0]["id"]40if len(companies) > 1:41company_id = airplane.prompt({42"company_id": Annotated[int, airplane.ParamConfig(43name="Company",44options=[airplane.LabeledOption(label=c["name"], value=c["id"]) for c in companies],45default=company_id,46)]47})["company_id"]4849return company_id
With that done, let's move on to upgrading this company.
Execute tasks
Execute tasks
Let's say another coworker already maintains a task for upgrading companies by ID. We don't want to
re-implement that logic, so we'll call their task. For this demo, go ahead and define that task:
typescriptCopied1import airplane from "airplane";23export default airplane.task(/* ... */);45export async function lookupCompany(query: string) {6/* ... */7}89export const upgradeCompanyByID = airplane.task(10{11slug: "demo_upgrade_company_by_id",12name: "Demo: Upgrade company by ID",13parameters: {14company_id: {15type: "integer",16name: "Company ID",17description: "The ID of the company to upgrade.",18},19num_seats: {20type: "integer",21name: "New seat count",22description: "How many total seats the company should have once upgraded.",23},24},25},26async (params) => {27console.log("Performing upgrade...");28console.log("Done!");2930return { companyID: params.company_id, numSeats: params.num_seats };31},32);
javascriptCopied1import airplane from "airplane";23export default airplane.task(/* ... */);45export async function lookupCompany(query) {6/* ... */7}89export const upgradeCompanyByID = airplane.task(10{11slug: "demo_upgrade_company_by_id",12name: "Demo: Upgrade company by ID",13parameters: {14company_id: {15type: "integer",16name: "Company ID",17description: "The ID of the company to upgrade.",18},19num_seats: {20type: "integer",21name: "New seat count",22description: "How many total seats the company should have once upgraded.",23},24},25},26async (params) => {27console.log("Performing upgrade...");28console.log("Done!");2930return { companyID: params.company_id, numSeats: params.num_seats };31},32);
pythonCopied1import airplane23@airplane.task(name="Demo: Upgrade company by ID")4def demo_upgrade_company_by_id(5company_id: int,6num_seats: int,7):8"""Upgrade a company's billing plan.910Args:11company_id: The ID of the company to upgrade.12num_seats: How many total seats the company should have once upgraded.13"""14print("Performing upgrade...")15print("Done!")1617return {"companyID": company_id, "numSeats": num_seats}
Since we declared the task using inline configuration, we can execute it with a
direct function call! This is syntactic sugar for performing an
airplane.execute
call; the task is still executed
and a run is still created.typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17num_seats: {18type: "integer",19name: "New seat count",20description: "How many total seats the company should have once upgraded.",21default: 10,22},23},24},25async (params) => {26const companyID = await lookupCompany(params.company);2728const result = await upgradeCompanyByID({29company_id: companyID,30num_seats: params.num_seats,31});3233return result.output;34},35);3637export async function lookupCompany(query: string) {38/* ... */39}4041export const upgradeCompanyByID = airplane.task(/* ... */);
javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17num_seats: {18type: "integer",19name: "New seat count",20description: "How many total seats the company should have once upgraded.",21default: 10,22},23},24},25async (params) => {26const companyID = await lookupCompany(params.company);2728const result = await upgradeCompanyByID({29company_id: companyID,30num_seats: params.num_seats,31});3233return result.output;34},35);3637export async function lookupCompany(query) {38/* ... */39}4041export const upgradeCompanyByID = airplane.task(/* ... */);
pythonCopied1import airplane234@airplane.task(5resources=[airplane.Resource("demo_db")],6)7def demo_upgrade_company(8company: str = "",9new_seat_count: int = 10,10):11"""Upgrade a company's billing plan.1213Args:14company: Search query to find a company. Matches on company ID and name.15new_seat_count: How many total seats the company should have once upgraded.16"""17company_id = lookup_company(company)1819result = demo_upgrade_company_by_id(company_id, new_seat_count)20return result.output
Keep in mind that you can execute any task, e.g. Shell tasks, using
airplane.execute
directly. All you need is the task slug, which you can find in the header on the
task's page:
Go ahead and execute the task and you'll see a new child task run:

Fantastic! 🎉 Our task now executes end-to-end and can "upgrade" a company's billing plan.
Handle failures
Handle failures
What happens if the upgrade task fails? By default, run failures will throw a
error
which bubbles up and marks the task as failed.
However, you can easily catch this error and handle (or ignore) it!Let's simulate a bug in the underlying task:
typescriptCopied1import airplane from "airplane";23export default airplane.task(/* ... */);45export async function lookupCompany(query: string) {6/* ... */7}89export const upgradeCompanyByID = airplane.task(10{11slug: "demo_upgrade_company_by_id",12name: "Demo: Upgrade company by ID",13parameters: {14company_id: {15type: "integer",16name: "Company ID",17description: "The ID of the company to upgrade.",18},19num_seats: {20type: "integer",21name: "New seat count",22description: "How many total seats the company should have once upgraded.",23},24},25},26async (params) => {27console.log("Performing upgrade...");28throw new Error("Request timed out");29console.log("Done!");3031return { companyID: params.company_id, numSeats: params.num_seats };32},33);
javascriptCopied1import airplane from "airplane";23export default airplane.task(/* ... */);45export async function lookupCompany(query) {6/* ... */7}89export const upgradeCompanyByID = airplane.task(10{11slug: "demo_upgrade_company_by_id",12name: "Demo: Upgrade company by ID",13parameters: {14company_id: {15type: "integer",16name: "Company ID",17description: "The ID of the company to upgrade.",18},19num_seats: {20type: "integer",21name: "New seat count",22description: "How many total seats the company should have once upgraded.",23},24},25},26async (params) => {27console.log("Performing upgrade...");28throw new Error("Request timed out");29console.log("Done!");3031return { companyID: params.company_id, numSeats: params.num_seats };32},33);
pythonCopied1import airplane234@airplane.task(name="Demo: Upgrade company by ID")5def demo_upgrade_company_by_id(6company_id: int,7num_seats: int,8):9"""Upgrade a company's billing plan.1011Args:12company_id: The ID of the company to upgrade.13num_seats: How many total seats the company should have once upgraded.14"""15print("Performing upgrade...")16raise Exception("Request timed out")17print("Done!")1819return {"companyID": company_id, "numSeats": num_seats}
Go ahead and execute the task again. As you'll see, the task failed with our new error:

Let's handle this error from our task with exception handling and send a nicely formatted error
message to the team via Slack!
To run the next example, you'll need to have
Slack connected to Airplane.
If your team does not use Slack, you can replace this example with a
console.log
or you can try
our email built-ins instead.typescriptCopied1import airplane, { RunTerminationError } from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17num_seats: {18type: "integer",19name: "New seat count",20description: "How many total seats the company should have once upgraded.",21default: 10,22},23},24},25async (params) => {26const companyID = await lookupCompany(params.company);2728try {29const result = await upgradeCompanyByID({30company_id: companyID,31num_seats: params.num_seats,32});3334return result.output;35} catch (err) {36const errMessage = err instanceof RunTerminationError ? err.run.output.error : String(err);37await airplane.slack.message(38// Swap this with any Slack channel. If the channel is private, you'll need to ensure the39// Airplane Slack user is invited.40"#test-airplane",41// You can use Slack markdown here:42// https://api.slack.com/reference/surfaces/formatting43`Failed to upgrade company "${company.name}". <https://app.airplane.dev/runs/${process.env.AIRPLANE_RUN_ID}|View in Airplane>.\n\n\`\`\`\n${errMessage}\n\`\`\`\n`,44);45throw err;46}47},48);4950export async function lookupCompany(query: string) {51/* ... */52}5354export const upgradeCompanyByID = airplane.task(/* ... */);
javascriptCopied1import airplane, { RunTerminationError } from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9parameters: {10company: {11type: "shorttext",12name: "Company",13description: "Search query to find a company. Matches on company ID and name.",14required: false,15default: "",16},17num_seats: {18type: "integer",19name: "New seat count",20description: "How many total seats the company should have once upgraded.",21default: 10,22},23},24},25async (params) => {26const companyID = await lookupCompany(params.company);2728try {29const result = await upgradeCompanyByID({30company_id: companyID,31num_seats: params.num_seats,32});3334return result.output;35} catch (err) {36const errMessage = err instanceof RunTerminationError ? err.run.output.error : String(err);37await airplane.slack.message(38// Swap this with any Slack channel. If the channel is private, you'll need to ensure the39// Airplane Slack user is invited.40"#test-airplane",41// You can use Slack markdown here:42// https://api.slack.com/reference/surfaces/formatting43`Failed to upgrade company "${company.name}". <https://app.airplane.dev/runs/${process.env.AIRPLANE_RUN_ID}|View in Airplane>.\n\n\`\`\`\n${errMessage}\n\`\`\`\n`,44);45throw err;46}47},48);4950export async function lookupCompany(query) {51/* ... */52}5354export const upgradeCompanyByID = airplane.task(/* ... */);
pythonCopied1import os23import airplane456@airplane.task(7resources=[airplane.Resource("demo_db")],8)9def demo_upgrade_company(10company: str = "",11new_seat_count: int = 10,12):13"""Upgrade a company's billing plan.1415Args:16company: Search query to find a company. Matches on company ID and name.17new_seat_count: How many total seats the company should have once upgraded.18"""19company_id = lookup_company(company)2021try:22result = demo_upgrade_company_by_id(company_id, new_seat_count)23return result.output24except Exception as e:25airplane.slack.message(26# Swap this with any Slack channel. If the channel is private, you'll need to ensure the27# Airplane Slack user is invited.28"#test-airplane",29# You can use Slack markdown here:30# https://api.slack.com/reference/surfaces/formatting31(32f"""Failed to upgrade company "{company["name"]}". """33f"""<https://app.airplane.dev/runs/{os.environ["AIRPLANE_RUN_ID"]}|"""34f"""View in Airplane>.\n\n```\n{e}\n```\n"""35),36)37raise
Give this task another run and you'll see a Slack message appear:

Switching runtimes
Switching runtimes
The workflow runtime for Airplane Tasks is currently in beta, and minor details
may change. We'd love to hear any feedback or requests at hello@airplane.dev.
By default, tasks run on the standard runtime which means that your script will
continue to run in the background while waiting for a user to select a company.
By switching the top-level task to the workflow runtime, Airplane can automatically pause the run
while waiting for operations to complete (e.g. waiting for the prompt to be submitted or a child run
to complete).
In order pause and resume workflow runs, the workflow runtime applies a few restrictions on what
workflow code can do. To learn more, see the workflow runtime overview.
To switch, set the
runtime
field. You won't see any changes when running this task locally, but
the task will now be able to wait much longer for a response—up to 60 days!typescriptCopied1import airplane, { RunTerminationError } from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9runtime: "workflow",10parameters: {11company: {12type: "shorttext",13name: "Company",14description: "Search query to find a company. Matches on company ID and name.",15required: false,16default: "",17},18num_seats: {19type: "integer",20name: "New seat count",21description: "How many total seats the company should have once upgraded.",22default: 10,23},24},25},26async (params) => {27const companyID = await lookupCompany(params.company);2829try {30const result = await upgradeCompanyByID({31company_id: companyID,32num_seats: params.num_seats,33});3435return result.output;36} catch (err) {37const errMessage = err instanceof RunTerminationError ? err.run.output.error : String(err);38await airplane.slack.message(39// Swap this with any Slack channel. If the channel is private, you'll need to ensure the40// Airplane Slack user is invited.41"#test-airplane",42// You can use Slack markdown here:43// https://api.slack.com/reference/surfaces/formatting44`Failed to upgrade company "${company.name}". <https://app.airplane.dev/runs/${process.env.AIRPLANE_RUN_ID}|View in Airplane>.\n\n\`\`\`\n${errMessage}\n\`\`\`\n`,45);46throw err;47}48},49);5051export async function lookupCompany(query: string) {52/* ... */53}5455export const upgradeCompanyByID = airplane.task(/* ... */);
javascriptCopied1import airplane, { RunTerminationError } from "airplane";23export default airplane.task(4{5slug: "demo_upgrade_company",6name: "Demo: Upgrade company",7// Grant this task access to the Demo DB:8resources: ["demo_db"],9runtime: "workflow",10parameters: {11company: {12type: "shorttext",13name: "Company",14description: "Search query to find a company. Matches on company ID and name.",15required: false,16default: "",17},18num_seats: {19type: "integer",20name: "New seat count",21description: "How many total seats the company should have once upgraded.",22default: 10,23},24},25},26async (params) => {27const companyID = await lookupCompany(params.company);2829try {30const result = await upgradeCompanyByID({31company_id: companyID,32num_seats: params.num_seats,33});3435return result.output;36} catch (err) {37const errMessage = err instanceof RunTerminationError ? err.run.output.error : String(err);38await airplane.slack.message(39// Swap this with any Slack channel. If the channel is private, you'll need to ensure the40// Airplane Slack user is invited.41"#test-airplane",42// You can use Slack markdown here:43// https://api.slack.com/reference/surfaces/formatting44`Failed to upgrade company "${company.name}". <https://app.airplane.dev/runs/${process.env.AIRPLANE_RUN_ID}|View in Airplane>.\n\n\`\`\`\n${errMessage}\n\`\`\`\n`,45);46throw err;47}48},49);5051export async function lookupCompany(query) {52/* ... */53}5455export const upgradeCompanyByID = airplane.task(/* ... */);
The Python SDK does not yet support the workflow runtime yet. This section is optional, so go ahead
and continue with the next section!
Deploy to Airplane
Deploy to Airplane
To wrap things up, go ahead and deploy your task:
shellCopied1$ airplane deploy
Once deployed, go to your Library to see your task in action!
Wrapping up
Wrapping up
As you've now seen, Tasks support many features for modeling complex processes. Processes that were
previously built with graph-based orchestration tools can instead be written with code using Tasks!
To learn more, see: