Send email from Snowflake without exporting customer data to a separate application by combining Snowflake’s change-data-capture features with Volanea’s REST email API. The reliable approach is not a marketplace app or native Snowflake plug-in: it is a controlled outbox pipeline built with a stream, a triggered task, a stored procedure, and Snowflake External Access.
Snowflake is a data platform rather than a marketing automation tool, so it does not provide a built-in “send email” action in the way a CRM or no-code workflow product might. That distinction matters. A dependable implementation starts with a row written to an email outbox table, uses a Snowflake stream to detect that new row, then invokes Volanea from a Python stored procedure that is explicitly allowed to make outbound HTTPS requests.
This guide shows the direct route. It keeps the Volanea API key in a Snowflake secret, maps concrete outbox fields into a Volanea email request, and explains the delivery, retry, and duplicate-send risks that appear at this specific Snowflake-to-API boundary.
The Snowflake model: streams and tasks, not a native email app
There is no native Volanea Snowflake app, Snowflake Marketplace listing, or one-click plug-in to install for this workflow. Do not look for an “install integration” button or assume that an email provider can be called directly from an ordinary worksheet query.
The Snowflake-native building blocks are:
- A table that records email work to perform.
- A stream that records change data capture (CDC) for that table.
- A triggered task whose
WHENcondition checks whether the stream has data. - A Python stored procedure that uses an External Access Integration to call Volanea over HTTPS.
- A secret object that supplies the Volanea API key only at runtime inside Snowflake.
The concrete trigger is therefore an INSERT into your email outbox table. Snowflake records that INSERT in the stream. A triggered task runs when SYSTEM$STREAM_HAS_DATA() reports data in that stream. That task calls the procedure that queues and sends the email.
This model is better than attaching email sends directly to analytical transformations. It makes email work explicit, reviewable, and recoverable. An invoice pipeline, for example, can insert a single row into EMAIL_OUTBOX after an invoice reaches a final state. An account workflow can insert a welcome-email row after a new verified account record lands in Snowflake. The send request becomes data with an audit trail rather than an invisible side effect of a query.
Choose an outbox event that represents a real business decision
Before writing Snowflake SQL, decide what event earns an email. The best trigger is usually not “a row changed somewhere.” It is a narrow, intentional business event that your data model can describe unambiguously.
For this guide, the event is:
A new row is inserted into
EMAIL_OUTBOXafter an application or data workflow decides that a recipient should receive a transactional message.
That event is deliberately separate from a customer table, order table, or product-usage table. Updating a customer’s phone number should not accidentally re-send a welcome email. Reprocessing historical order data should not accidentally send thousands of old receipts.
A practical outbox table includes both content and controls:
CREATE OR REPLACE TABLE EMAIL_OUTBOX (
MESSAGE_ID STRING NOT NULL,
RECIPIENT_EMAIL STRING NOT NULL,
SUBJECT STRING NOT NULL,
HTML_BODY STRING,
TEXT_BODY STRING,
TEMPLATE_NAME STRING,
EVENT_TYPE STRING NOT NULL,
CREATED_AT TIMESTAMP_NTZ NOT NULL DEFAULT CURRENT_TIMESTAMP(),
PRIMARY KEY (MESSAGE_ID)
);
MESSAGE_ID is essential. Generate it in the application or upstream workflow before inserting the row. It should remain stable if the upstream process retries. A UUID is a common choice, but a deterministic identifier can also work when the underlying event has a natural unique key, such as invoice:INV-10492:receipt.
The table stores both HTML_BODY and TEXT_BODY so the email has a plain-text alternative. TEMPLATE_NAME is optional but useful for reporting and routing. EVENT_TYPE lets you distinguish operational categories such as password_reset, invoice_receipt, trial_expiring, or data_export_ready.
Do not use an analyst’s ad hoc query result as the email trigger unless it writes a deliberate outbox row. That simple boundary prevents an exploratory or re-run query from becoming an unintended production mailing.
Create the stream that detects new email requests
A Snowflake stream tracks data changes made to a table. Create one on the outbox table:
CREATE OR REPLACE STREAM EMAIL_OUTBOX_STREAM
ON TABLE EMAIL_OUTBOX;
When a new outbox record is inserted, the stream exposes that change until a consuming DML operation advances the stream offset. A stream includes Snowflake metadata columns such as METADATA$ACTION and METADATA$ISUPDATE, in addition to the columns from the underlying table.
For an append-only email outbox, your sender should process only new INSERT records. That avoids treating an outbox-row correction as a second instruction to send. In the procedure below, the ingestion step filters for rows where:
METADATA$ACTION = 'INSERT'
AND METADATA$ISUPDATE = FALSE
That condition matters because Snowflake represents some updates as paired delete-and-insert changes in a standard stream. If you do not distinguish original inserts from update-generated rows, correcting the subject line or recipient field could result in an unexpected duplicate email.
A stream is not itself a queue with an infinite history. It has a change-tracking offset, and its retention behavior depends on the table’s retention settings and account configuration. Treat it as the handoff between your source table and a durable dispatch ledger, not as your long-term email audit store.
Create a durable dispatch ledger before making HTTP calls
Calling an external API directly while reading a stream creates a difficult failure mode: the HTTP request may succeed, but Snowflake may fail before recording that it succeeded. If the task runs again, the same stream row can be selected again.
The right response is to separate capture from dispatch. First consume stream rows into a durable EMAIL_DISPATCH table. Then send pending rows from that table. The stream advances after the ledger write, while the ledger preserves the work even if a later API request times out or fails.
CREATE OR REPLACE TABLE EMAIL_DISPATCH (
MESSAGE_ID STRING NOT NULL,
RECIPIENT_EMAIL STRING NOT NULL,
SUBJECT STRING NOT NULL,
HTML_BODY STRING,
TEXT_BODY STRING,
TEMPLATE_NAME STRING,
EVENT_TYPE STRING NOT NULL,
CREATED_AT TIMESTAMP_NTZ NOT NULL,
STATUS STRING NOT NULL DEFAULT 'PENDING',
ATTEMPT_COUNT NUMBER NOT NULL DEFAULT 0,
LAST_ATTEMPT_AT TIMESTAMP_NTZ,
VOLANEA_MESSAGE_ID STRING,
LAST_ERROR STRING,
SENT_AT TIMESTAMP_NTZ,
PRIMARY KEY (MESSAGE_ID)
);
The dispatch table gives your operations team a clear answer to important questions:
- Which messages are waiting to be sent?
- Which messages failed and why?
- How many attempts has each message made?
- Which Volanea message ID was returned for a successful request?
- Did a specific business event produce one email or several?
A primary key declaration in Snowflake is generally informational unless you use a constraint-enforcing table type, so the procedure should still use MERGE on MESSAGE_ID. The MERGE is the practical guard that prevents a stream retry from creating a second dispatch row.
Allow Snowflake to reach Volanea securely
Snowflake does not permit stored procedures to call arbitrary internet hosts by default. For outbound HTTPS, configure an External Access Integration. It combines a network rule that identifies the allowed destination with a secret that the procedure may use.
The following SQL uses api.volanea.com on port 443 as the only allowed external host. Run account-level setup with an appropriately privileged role, then grant only the access needed by the role that owns or executes the procedure.
CREATE OR REPLACE NETWORK RULE VOLANEA_API_NETWORK_RULE
MODE = EGRESS
TYPE = HOST_PORT
VALUE_LIST = ('api.volanea.com:443');
CREATE OR REPLACE SECRET VOLANEA_API_KEY
TYPE = GENERIC_STRING
SECRET_STRING = '<store-your-volanea-api-key-here>';
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION VOLANEA_EXTERNAL_ACCESS
ALLOWED_NETWORK_RULES = (VOLANEA_API_NETWORK_RULE)
ALLOWED_AUTHENTICATION_SECRETS = (VOLANEA_API_KEY)
ENABLED = TRUE;
Replace the placeholder only in a controlled deployment step. Do not paste the key into a worksheet, source repository, dbt model, task definition, browser-visible configuration, or JSON payload column. Snowflake secrets are designed to keep sensitive values out of ordinary query text and application configuration.
The procedure retrieves the generic secret at runtime using Snowflake’s _snowflake module. The key is then used only to construct the outbound request header. Users who can inspect a client-side application, a browser network trace, or a shared configuration file must never be able to retrieve it.
For a production deployment, restrict ownership carefully. A role that can alter the stored procedure or replace the secret reference can potentially redirect requests or change behavior. Separate account-level integration administration from day-to-day data operations where possible.
Build the Snowflake procedure that maps rows to Volanea emails
The procedure below performs two jobs:
- It merges newly inserted stream records into
EMAIL_DISPATCH, consuming those stream records safely. - It sends a limited batch of pending dispatch records to Volanea.
The Volanea request maps each Snowflake dispatch row to the REST email payload:
| Snowflake field | Volanea email field | Purpose |
|---|---|---|
RECIPIENT_EMAIL | to | Recipient address |
| fixed verified sender | from | Your Volanea-approved sender identity |
SUBJECT | subject | Message subject |
HTML_BODY | html | HTML content |
TEXT_BODY | text | Plain-text alternative |
Use a sender address from a domain you have verified in Volanea. The exact verification and sender setup steps are documented in the email API setup guides, and they should be completed before this task is enabled.
CREATE OR REPLACE PROCEDURE PROCESS_VOLANEA_EMAIL_DISPATCH()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('snowflake-snowpark-python', 'requests')
HANDLER = 'run'
EXTERNAL_ACCESS_INTEGRATIONS = (VOLANEA_EXTERNAL_ACCESS)
SECRETS = ('volanea_api_key' = VOLANEA_API_KEY)
EXECUTE AS OWNER
AS
$$
import json
import requests
import _snowflake
from snowflake.snowpark import Session
VOLANEA_ENDPOINT = "https://api.volanea.com/v1/emails"
FROM_ADDRESS = "Notifications <notifications@your-verified-domain.com>"
BATCH_SIZE = 100
TIMEOUT_SECONDS = 15
def run(session: Session):
# Step 1: Consume new stream records into the durable dispatch ledger.
session.sql("""
MERGE INTO EMAIL_DISPATCH AS target
USING (
SELECT
MESSAGE_ID,
RECIPIENT_EMAIL,
SUBJECT,
HTML_BODY,
TEXT_BODY,
TEMPLATE_NAME,
EVENT_TYPE,
CREATED_AT
FROM EMAIL_OUTBOX_STREAM
WHERE METADATA$ACTION = 'INSERT'
AND METADATA$ISUPDATE = FALSE
) AS source
ON target.MESSAGE_ID = source.MESSAGE_ID
WHEN NOT MATCHED THEN INSERT (
MESSAGE_ID,
RECIPIENT_EMAIL,
SUBJECT,
HTML_BODY,
TEXT_BODY,
TEMPLATE_NAME,
EVENT_TYPE,
CREATED_AT,
STATUS,
ATTEMPT_COUNT
) VALUES (
source.MESSAGE_ID,
source.RECIPIENT_EMAIL,
source.SUBJECT,
source.HTML_BODY,
source.TEXT_BODY,
source.TEMPLATE_NAME,
source.EVENT_TYPE,
source.CREATED_AT,
'PENDING',
0
)
""").collect()
api_key = _snowflake.get_generic_secret_string('volanea_api_key')
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
pending = session.sql(f"""
SELECT
MESSAGE_ID,
RECIPIENT_EMAIL,
SUBJECT,
HTML_BODY,
TEXT_BODY
FROM EMAIL_DISPATCH
WHERE STATUS IN ('PENDING', 'RETRY')
AND ATTEMPT_COUNT < 5
ORDER BY CREATED_AT
LIMIT {BATCH_SIZE}
""").collect()
sent = 0
failed = 0
for row in pending:
message_id = row["MESSAGE_ID"]
payload = {
"from": FROM_ADDRESS,
"to": [row["RECIPIENT_EMAIL"]],
"subject": row["SUBJECT"],
"html": row["HTML_BODY"],
"text": row["TEXT_BODY"]
}
session.sql("""
UPDATE EMAIL_DISPATCH
SET STATUS = 'SENDING',
ATTEMPT_COUNT = ATTEMPT_COUNT + 1,
LAST_ATTEMPT_AT = CURRENT_TIMESTAMP(),
LAST_ERROR = NULL
WHERE MESSAGE_ID = ?
""", params=[message_id]).collect()
try:
response = requests.post(
VOLANEA_ENDPOINT,
headers=headers,
data=json.dumps(payload),
timeout=TIMEOUT_SECONDS
)
if 200 <= response.status_code < 300:
response_body = response.json()
volanea_message_id = response_body.get("id")
session.sql("""
UPDATE EMAIL_DISPATCH
SET STATUS = 'SENT',
VOLANEA_MESSAGE_ID = ?,
SENT_AT = CURRENT_TIMESTAMP(),
LAST_ERROR = NULL
WHERE MESSAGE_ID = ?
""", params=[volanea_message_id, message_id]).collect()
sent += 1
else:
error_text = response.text[:2000]
session.sql("""
UPDATE EMAIL_DISPATCH
SET STATUS = 'RETRY',
LAST_ERROR = ?
WHERE MESSAGE_ID = ?
""", params=[error_text, message_id]).collect()
failed += 1
except requests.RequestException as error:
session.sql("""
UPDATE EMAIL_DISPATCH
SET STATUS = 'RETRY',
LAST_ERROR = ?
WHERE MESSAGE_ID = ?
""", params=[str(error)[:2000], message_id]).collect()
failed += 1
return {
"queued_or_existing": "stream rows merged into EMAIL_DISPATCH",
"sent": sent,
"failed": failed,
"checked": len(pending)
}
$$;
The procedure deliberately does not trust a successful Python execution alone as proof of a delivered email. A successful HTTP response means Volanea accepted the API request. Inbox placement, delivery, bounces, and complaints are later delivery events, not the same thing as request acceptance.
The procedure also does not put the Volanea key in the payload, query result, or dispatch table. Only the runtime process reads it from VOLANEA_API_KEY.
Start the pipeline with a triggered Snowflake task
Create a task that runs when the stream has data:
CREATE OR REPLACE TASK PROCESS_VOLANEA_EMAIL_TASK
WAREHOUSE = EMAIL_AUTOMATION_WH
WHEN SYSTEM$STREAM_HAS_DATA('EMAIL_OUTBOX_STREAM')
AS
CALL PROCESS_VOLANEA_EMAIL_DISPATCH();
ALTER TASK PROCESS_VOLANEA_EMAIL_TASK RESUME;
This is the concrete Snowflake trigger path:
INSERT into EMAIL_OUTBOX
→ EMAIL_OUTBOX_STREAM receives an INSERT change
→ SYSTEM$STREAM_HAS_DATA('EMAIL_OUTBOX_STREAM') returns true
→ PROCESS_VOLANEA_EMAIL_TASK runs
→ procedure writes to EMAIL_DISPATCH
→ procedure calls Volanea
A triggered task is efficient for new work because it does not need to wake a warehouse merely to discover that there are no new outbox rows. However, the example procedure can leave records in RETRY after a temporary Volanea, DNS, network, or payload failure. Those retries need another opportunity to run even when the stream has no fresh records.
Add a scheduled recovery task for retryable records:
CREATE OR REPLACE TASK RETRY_VOLANEA_EMAIL_TASK
WAREHOUSE = EMAIL_AUTOMATION_WH
SCHEDULE = '5 MINUTE'
AS
CALL PROCESS_VOLANEA_EMAIL_DISPATCH();
ALTER TASK RETRY_VOLANEA_EMAIL_TASK RESUME;
For high-volume systems, consider separating stream ingestion and dispatch into two procedures or task graphs. That lets you tune batching, rate limits, retry intervals, and monitoring independently. For modest transactional volumes, the combined pattern is easier to understand and operate.
Validate recipient data before it reaches the sender
Snowflake can store a value that looks like an email address but is not deliverable, belongs to an old account, or contains accidental whitespace. Clean the address before inserting into EMAIL_OUTBOX, not only after the API rejects it.
At a minimum, normalize the address and reject empty values:
INSERT INTO EMAIL_OUTBOX (
MESSAGE_ID,
RECIPIENT_EMAIL,
SUBJECT,
HTML_BODY,
TEXT_BODY,
EVENT_TYPE
)
SELECT
UUID_STRING(),
LOWER(TRIM(CUSTOMER_EMAIL)),
'Your export is ready',
'<p>Your export is ready to download.</p>',
'Your export is ready to download.',
'data_export_ready'
FROM EXPORT_REQUESTS
WHERE CUSTOMER_EMAIL IS NOT NULL
AND TRIM(CUSTOMER_EMAIL) <> '';
For transactional messages, validate at account creation or immediately before critical sends when your risk model justifies it. Volanea’s email address verification tool can help catch malformed or risky recipient addresses before they become repeated API errors, bounces, or poor sender reputation signals.
Avoid using a simplistic regular expression as your sole deliverability control. Syntax validation does not establish that a mailbox exists, that the recipient expects the message, or that your organization is permitted to contact them. Consent and business purpose still belong in your source-of-truth application data.
When this breaks: Snowflake-to-Volanea failure modes
Every external-send design must acknowledge that a network boundary prevents perfect exactly-once delivery. Snowflake can know that it attempted an HTTP request. Volanea can know that it received one. A timeout between those two observations can leave both systems with incomplete information.
Snowflake retries can create duplicate sends
A task can run again after a failure, an operational rerun, or a transient error. A procedure can also time out after Volanea has already accepted the email request. If your code sends immediately from a stream with no durable ledger, the same record may be selected again and sent twice.
The dispatch ledger reduces this risk by giving each message a stable MESSAGE_ID, recording status, and using MERGE rather than blindly inserting each stream row. For stronger duplicate protection, use Volanea’s idempotency capability if it is available for your API endpoint and account, passing MESSAGE_ID as the idempotency value according to the current API reference. Keep your own dispatch ledger even when provider-side idempotency is available: it remains your internal audit and recovery record.
Do not solve duplicates by marking a message SENT before calling Volanea. That converts a duplicate risk into a message-loss risk. If Snowflake records SENT first and the outbound request never happens, the recipient never receives the message and the system has no retry path.
HTTP timeouts are ambiguous
A requests timeout does not prove Volanea rejected the email. It means the Snowflake procedure did not receive a complete response within the timeout window. The provider may have accepted the request, may still be processing it, or may never have received it.
For this reason, treat timeouts as retryable but potentially duplicate-producing. Record the error, preserve the same MESSAGE_ID, and investigate repeated timeout patterns. Use a short but realistic timeout such as 10 to 20 seconds; an extremely long timeout holds compute unnecessarily, while an extremely short timeout converts normal network variation into avoidable retries.
Missing payload fields can stop some records
Not every upstream event contains the fields required for every email. One source may have RECIPIENT_EMAIL; another may only have a user ID. One workflow may generate HTML; another may only have a text summary. If an upstream system, connector, or plan tier omits fields you expected, do not let the sender build a malformed request silently.
Make missing-field handling explicit before the API call. For example, mark a row FAILED_VALIDATION when RECIPIENT_EMAIL or SUBJECT is null, or when both HTML_BODY and TEXT_BODY are empty. That status should be non-retryable until data is fixed. A retry task cannot repair missing content.
Sender identity failures are configuration failures
A Volanea API request can fail if the from address is not aligned with a verified sending domain or permitted sender identity. This is not a transient failure. Retrying it every five minutes only creates noise.
Classify response errors where possible. Authentication failures, sender-verification failures, and malformed payloads should become FAILED_CONFIGURATION or FAILED_VALIDATION. Temporary server errors and rate limits should remain RETRY with a controlled backoff policy.
Stream retention and paused tasks can leave gaps
If a task remains suspended for too long, or if stream data is not consumed within the applicable retention window, your pipeline can become harder to reconcile. Monitor task history, stream freshness, and the age of the oldest PENDING dispatch record.
The dispatch ledger gives you a practical recovery target. Even if you must rebuild a stream or repair task configuration, you can re-drive records from EMAIL_DISPATCH based on their status rather than attempting to reconstruct every email from raw warehouse changes.
Operational controls for a production email pipeline
A direct Snowflake integration should be treated as production infrastructure, not a convenience script. Add observability before relying on it for password resets, receipts, security notices, or other customer-facing communications.
Track at least these measures:
- Count of
PENDING,SENDING,RETRY,SENT, and terminal-failure records. - Age of the oldest unsent message.
- Attempts per message and retry rate by event type.
- Volanea HTTP status codes and API response IDs.
- Snowflake task failures, run duration, and skipped executions.
- Recipient-domain patterns for bounces or provider rejections.
A simple operational query can highlight stalled emails:
SELECT
STATUS,
COUNT(*) AS MESSAGE_COUNT,
MIN(CREATED_AT) AS OLDEST_MESSAGE,
MAX(ATTEMPT_COUNT) AS MAX_ATTEMPTS
FROM EMAIL_DISPATCH
WHERE STATUS <> 'SENT'
GROUP BY STATUS
ORDER BY OLDEST_MESSAGE;
Set a clear retry policy. For example, allow five attempts for transient errors, use progressively longer delays, and move exhausted messages to a terminal FAILED state for review. A single fixed five-minute retry can overload a provider during an outage and make incident recovery harder.
Also separate transactional sends from campaign sends. Transactional messages are typically triggered by a specific user action or system state and need prompt processing. Promotional or lifecycle campaigns usually need audience controls, unsubscribe logic, suppression management, and scheduling that should not be improvised inside a generic Snowflake task.
Direct Snowflake integration versus middleware
The direct External Access approach is appropriate when Snowflake is the authoritative place where the event becomes final and your team can operate SQL, tasks, stored procedures, secrets, and error handling.
Use the direct route when:
- The email trigger is derived from Snowflake data.
- You need data to remain inside a controlled warehouse workflow until the send request is made.
- You can define a durable outbox and monitor task execution.
- You need an auditable relationship between source events and email requests.
A middleware route using a workflow platform can make sense when the event originates in a SaaS product and Snowflake is only one destination. In that design, the source application triggers the middleware, which calls Volanea and optionally writes delivery or event data back to Snowflake.
The trade-off is control. Middleware can be faster to configure, but it introduces another account, another credential store, another retry system, and potentially another copy of recipient data. It also does not eliminate duplicate-send concerns; it merely relocates them.
For warehouse-originated transactional notifications, the Snowflake outbox model is usually easier to audit because the business event, message content, dispatch state, and provider result can be traced through one data pipeline.
Conclusion
To send email from Snowflake with Volanea, use Snowflake as the event and orchestration layer, not as a pretend email marketplace. Insert an intentional email request into an outbox table, capture it with a stream, trigger a task with SYSTEM$STREAM_HAS_DATA, and call Volanea only through an External Access Integration and Snowflake secret.
The important engineering decision is the durable dispatch ledger. It gives you a record of every intended send, protects against stream reprocessing, supports retries, and makes ambiguous network failures visible rather than mysterious. Pair that ledger with verified senders, recipient validation, task monitoring, and a clear failure policy, and Snowflake can become a dependable source for transactional email events.
FAQ
Can Snowflake call the Volanea API directly?
Yes, through a Python stored procedure configured with a Snowflake External Access Integration, an egress network rule for Volanea, and a Snowflake secret for the API key. It is not a native Snowflake Marketplace installation or plug-in.
What triggers the email send in Snowflake?
In this pattern, an INSERT into EMAIL_OUTBOX is the business trigger. A stream detects that inserted row, and a triggered task runs when SYSTEM$STREAM_HAS_DATA('EMAIL_OUTBOX_STREAM') is true.
Where should the Volanea API key be stored?
Store it in a Snowflake SECRET object of type GENERIC_STRING, then expose it only to the stored procedure through the External Access Integration. Do not put it in frontend settings, source code, table columns, or task SQL.
Can a Snowflake retry send the same email twice?
It can if the provider accepted a request but Snowflake timed out before recording the response. Use a durable dispatch ledger keyed by a stable message ID, and use provider-supported idempotency controls where available to reduce duplicate risk.
Should every table update trigger an email?
Usually no. Insert a deliberate outbox record only after a defined business event is final. This avoids accidental emails caused by data corrections, backfills, model reruns, or unrelated updates.