Part 10
Operators & Integrations Catalog
54Compute & scripts#
What it does: The core scripting layer — BashOperator, PythonOperator, ExternalPythonOperator, PythonVirtualenvOperator, and BranchPythonOperator.
How it works:
- All five run as CPU-bound, local work, subject to the deployment's local-task concurrency cap — heavier fan-out of these operators competes for the same local-execution budget.
BashOperatorruns your command withbash -c "<your command>"from the orchestrator process's own working directory — not your DAGs repo checkout. Use an explicitcd $DAGS_REPO_PATH/...(or absolute paths) if your command needs to reference files checked out from Git.ExternalPythonOperator/PythonVirtualenvOperatorre-import your DAG module fresh inside the target interpreter — that interpreter/venv must be able toimport dag_parserand any packages your callable needs.BranchPythonOperatoris the only one of the five that changes the scheduling graph (feature 33) — the other four simply run and report success/failure.- None of the five inherit the orchestrator's own environment (feature 22), regardless of which one you pick.
Example:
# BashOperator: cwd is the orchestrator's own process dir, NOT the dags repo
fix_path_example = BashOperator(
task_id="fix_path_example",
bash_command="cd $DAGS_REPO_PATH/dags && ls -la", # explicit cd required
)55SQL databases#
What it does: Running a query against a data warehouse or database connection. SnowflakeOperator, SQLExecuteQueryOperator, MySqlOperator, PostgresOperator, and RedshiftOperator all share the same shape — sql= plus connection_id= — pick whichever matches your connection's type.
Parameters:
| Param | Type | Notes |
|---|---|---|
connection_id | string | Must reference an existing connection of the matching type. |
sql | string | The query to run. |
parameters | dict | Bound into the query as real parameterized-query placeholders. |
How it works:
- Use
SnowflakeOperatorfor Snowflake, andSQLExecuteQueryOperator(database-agnostic) or the type-specificMySqlOperator/PostgresOperator/RedshiftOperatorfor the matching database. parametersvalues are bound as real query parameters, not string-substituted into the SQL text — passing user-supplied values (e.g. from a manually triggered run'sconf) is safe without any manual escaping on your part.- Only the first result row is captured as
return_value(feature 37) — a query returning many rows drops everything after row 1. SnowflakeOperatormaintains a connection pool perconnection_idthat persists across task executions on the same worker — if you change a connection's credentials, an already-warm pool won't pick up the change until the worker restarts.
Example:
from dag_parser.dynamic.dag_context import PostgresOperator
extract = SnowflakeOperator(
task_id="extract",
connection_id="snowflake_prod",
sql="SELECT * FROM sales.orders WHERE region = %(region)s",
parameters={"region": "US-WEST"}, # safely bound, not string-substituted
)
load_summary = PostgresOperator(
task_id="load_summary",
connection_id="reporting_pg",
sql="INSERT INTO summary (region, total) VALUES (%(region)s, %(total)s)",
parameters={"region": "US-WEST", "total": 12000},
)56Data movement#
What it does: S3ToRedshiftOperator loads data from S3 into Redshift via COPY. GCSToBigQueryOperator loads data from GCS into BigQuery via a load job.
Parameters (S3ToRedshiftOperator):
| Param | Type | Notes |
|---|---|---|
s3_conn_id / iam_role | string | Provide exactly one — whichever is set determines the auth method used for the COPY. |
region | string | Falls back to the connection's stored region if omitted, in either auth mode. |
truncate | boolean | Runs in the same transaction as the following COPY. |
s3_key | string | Wildcards supported. |
Parameters (GCSToBigQueryOperator):
| Param | Type | Notes |
|---|---|---|
source_format | string | e.g. PARQUET, CSV. |
write_disposition | string | e.g. WRITE_TRUNCATE. |
autodetect | boolean | Schema auto-detection. Set exactly one of autodetect/schema_fields. |
schema_fields | list | Explicit schema. Set exactly one of autodetect/schema_fields. |
poll_interval | integer (seconds) | Polling cadence; the load job is bounded by the task's execution_timeout. |
How it works:
truncate=Trueand the subsequentCOPYrun inside a single transaction — ifCOPYfails for any reason, the truncate is rolled back and the table keeps its original data.- Set exactly one of
iam_role/s3_conn_id— the operator validates this at DAG-parse time. GCSToBigQueryOperator's poll loop tolerates a transient status-check error (retrying rather than failing the task immediately) and is bounded by the task'sexecution_timeout(feature 16) — set that explicitly if you need a hard cap on how long a load job is allowed to run.autodetectandschema_fieldsare mutually exclusive — set exactly one; the operator validates this at DAG-parse time.destination_project/destination_datasetfall back to the connection's stored defaults if omitted from the task — the same connection can target different datasets depending on whether a given task fills these in.
Example:
load_from_s3 = S3ToRedshiftOperator(
task_id="load_from_s3",
s3_conn_id="s3_default",
redshift_conn_id="redshift_prod",
s3_bucket="my-data-bucket",
s3_key="exports/2026/01/*.csv",
table="orders",
schema="public",
copy_options="CSV IGNOREHEADER 1 GZIP",
truncate=True, # truncate + COPY run as a single atomic transaction
)
load_from_gcs = GCSToBigQueryOperator(
task_id="load_from_gcs",
bigquery_conn_id="bq_prod",
source_bucket="my-gcs-bucket",
source_object="exports/*.parquet",
destination_dataset="analytics",
destination_table="orders",
source_format="PARQUET",
write_disposition="WRITE_TRUNCATE",
autodetect=True,
poll_interval=10,
execution_timeout=1800, # ceiling for the load job
)57HTTP (one-shot call)#
What it does: Makes a single HTTP request as a task step — distinct from HttpSensor (feature 46), which polls repeatedly waiting for a condition.
Parameters (HttpOperator):
| Param | Type | Notes |
|---|---|---|
url | string | Renders through Go templating (feature 38). |
method | string | e.g. GET, POST. |
headers | dict | |
body | string | |
connection_id | string | Optional — resolves stored auth for the target endpoint. |
How it works:
HttpOperator(also importable asSimpleHttpOperator) is a ready-made class — no need to subclassBaseOperatoryourself.url/headers/bodyrender through Go templating (feature 38), not Jinja.- A response is truncated at 1MB, and any 2xx counts as success — there's no response-body validation option here (that's
HttpSensor-only); add a follow-up Python task reading the pushedreturn_valueif you need to inspect the body.
Example:
from dag_parser.dynamic.operators import HttpOperator
notify_downstream = HttpOperator(
task_id="notify_downstream",
url="https://api.example.com/v1/events",
method="POST",
headers={"Content-Type": "application/json"},
connection_id="internal_api",
body='{"event": "pipeline_complete", "dag_id": "{{ .DagID }}"}',
)58SSH#
What it does: Opens an SSH connection and runs a single remote command.
Parameters:
| Param | Type | Notes |
|---|---|---|
connection_id | string | Provides host/auth (password or private key — key is tried first if both are present) and the expected host key/fingerprint. |
command | string | The remote command to run. |
environment | dict | Best-effort — see below. |
How it works:
- SSH connections verify the remote host's key against the fingerprint stored on the connection — set this when creating the connection so Maestro-Pi can detect an unexpected host on the other end.
environment={}may be silently ignored by the remote server depending on itssshd_config(most servers reject arbitrarySetEnvrequests unless explicitly allowlisted) — a rejected env var is only logged as a warning, never surfaced as a task error. Prefer passing values through the command string itself if you need to guarantee they arrive.- Output is
return_value'd as stdout and stderr combined into one string (feature 37) — there's no way to capture them separately from this operator. - The connection dial itself has a fixed 30-second timeout, separate from the command's own execution time — use
execution_timeout(feature 16) to bound the whole task if the command itself might run long.
Example:
from dag_parser.dynamic.operators import SSHOperator
remote_cleanup = SSHOperator(
task_id="remote_cleanup",
connection_id="prod_bastion", # host key verified against the connection's stored fingerprint
command="rm -rf /tmp/staging/{{ .DS }}",
environment={"STAGE": "prod"}, # may be silently dropped by sshd's AcceptEnv config
)59Email / Slack (as task steps)#
What it does: EmailOperator and SlackAPIPostOperator send a message as a normal task step in the graph — distinct from the callback-based alert channels in Part 7, which fire on an event rather than running as their own scheduled step.
Parameters (EmailOperator):
| Param | Type | Notes |
|---|---|---|
to | list of strings | required |
subject | string | Supports the same {{dag_id}}/{{task_id}}/{{run_id}} tokens as html_content. |
html_content | string | Supports the same tokens. |
cc / bcc | list of strings | Optional. |
from_email | string | Overrides the deployment's default SMTP "from" address for this task. |
files | list of file paths | Attached to the outgoing email. |
custom_headers | dict | Added to the outgoing MIME message. |
Parameters (SlackAPIPostOperator):
| Param | Type | Notes |
|---|---|---|
connection_id | string | Token-mode or webhook-mode. |
channel | string | Required with a token-mode connection; optional with webhook-mode (most webhooks are already bound to a fixed channel). |
text | string | Message body. |
unfurl_links | boolean | Only has an effect with a token-mode connection. |
How it works:
- Set
from_emailto override the "from" address for that specific task; leave it unset to use the deployment's globally-configured SMTP address. subjectandhtml_contentare both token-substituted the same way.filesattaches the listed local file paths to the outgoing email;custom_headersare added as extra header lines on the outgoing message.SlackAPIPostOperator'sreturn_valueshape depends on the connection's mode: webhook mode returns Slack's raw response body (typically"ok"); token mode returns{"ts": "<message timestamp>"}. A downstream task reading this XCom should know which mode the connection uses.
Example:
from dag_parser.dynamic.dag_context import EmailOperator
from dag_parser.dynamic.dag_context import SlackAPIPostOperator
send_report = EmailOperator(
task_id="send_report",
to=["team@company.com"],
subject="Report for {{dag_id}}", # substituted
html_content="<p>Report for {{ dag_id }} run {{ run_id }}</p>",
from_email="pipeline-alerts@company.com",
files=["/tmp/report.csv"],
custom_headers={"X-Priority": "1"},
)
post_to_slack = SlackAPIPostOperator(
task_id="post_to_slack",
connection_id="slack_bot_token", # token-mode: channel is REQUIRED
channel="#data-alerts",
text="Report generation complete",
unfurl_links=True,
)60Databricks#
What it does: DatabricksSubmitRunOperator submits a one-off run via the Databricks Jobs API, on either an existing or an ephemeral cluster, and waits for it to finish.
Parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
task_type | string | "notebook_task" | One of notebook_task, spark_python_task, spark_jar_task. |
cluster_id | string | connection's stored default, if any | Falls back to the connection's own extra.cluster_id if omitted. |
new_cluster | dict | none | Set exactly one of cluster_id/new_cluster — the operator validates this at DAG-parse time. |
poll_interval | integer (seconds) | — | Polling cadence; the poll loop tolerates a transient status-check error and is bounded by execution_timeout. |
idempotency_token | string | none | Passed unchanged on every retry attempt. |
How it works:
- Set exactly one of
cluster_idornew_clusterper task — setting both (or neither, with no connection default available) raises a clear validation error at DAG-parse time. cluster_idfalls back to the connection's own stored default if you omit it from the task — handy if most tasks target the same cluster.- The poll loop tolerates a single transient HTTP error (retrying rather than failing the task immediately) and is bounded by the task's
execution_timeout(feature 16) — set that explicitly to cap how long you're willing to wait for a Databricks job. - A
notebook_taskthat callsdbutils.notebook.exit(...)is the most reliable way to get a meaningfulreturn_valueback —spark_python_task/spark_jar_taskruns typically don't populate one the same way. - If you set a static
idempotency_tokenon a task that also hasretriesconfigured, a retry using the same token may be deduplicated by Databricks itself — only do this if you specifically want that dedup behavior.
Example:
from dag_parser.dynamic.dag_context import DatabricksSubmitRunOperator
run_notebook = DatabricksSubmitRunOperator(
task_id="run_notebook",
connection_id="databricks_prod",
task_type="notebook_task",
notebook_path="/Shared/etl/daily_aggregate",
base_parameters={"run_date": "{{ .DS }}"},
new_cluster={ # wins over any cluster_id/connection default if both are set
"spark_version": "14.3.x-scala2.12",
"node_type_id": "i3.xlarge",
"num_workers": 2,
},
poll_interval=15,
execution_timeout=3600, # bound the otherwise-unbounded poll loop
)61Cross-DAG orchestration, summarized#
What it does: A recap of the operators that compose multiple DAGs together or wait on state outside the current DAG's own task graph: TriggerDagRunOperator (feature 27) plus the four sensors (Part 8).
How it works:
TriggerDagRunOperator'swait_for_completion=Trueuses the same lightweight, slot-free waiting mechanism described in feature 27 — it does not hold a worker slot, unlike amode="poke"sensor. Itspoke_intervalcontrols how often the background reconciler checks the child run's state while waiting.TriggerDagRunOperatoris treated as lightweight coordination work for scheduling purposes, the same as the sensors — it is not subject to the same admission limits as CPU-heavy Python/Bash tasks, so it stays responsive even under system load.- The sensors (Part 8) each wait on a condition (an HTTP check, a SQL query, a clock, or another task/DAG's state) using the poke/reschedule model (feature 51).
Example:
from dag_parser.dynamic.dag_context import TriggerDagRunOperator
from dag_parser.dynamic.operators import ExternalTaskSensor
with DAG(dag_id="orchestrator", schedule="@daily", start_date=datetime(2026, 1, 1)) as dag:
trigger_child = TriggerDagRunOperator(
task_id="trigger_child",
trigger_dag_id="child_pipeline",
wait_for_completion=True,
poke_interval=30, # how often the reconciler checks the child run's state
)
wait_for_sibling = ExternalTaskSensor(
task_id="wait_for_sibling",
external_dag_id="sibling_pipeline",
allowed_states=["success"],
mode="reschedule",
)