Send Emails from Task Scripts
Send email from self-service pipeline task scripts by using the context.send_email() function
Task scripts can send email through the context.send_email() function. Use it to tell scientists that a pipeline finished, to escalate a failed run, or to deliver a generated report to a distribution list.
Your task script never handles mail credentials. It asks the Tetra Data Platform (TDP) to send the message, and TDP sends it from your organization's verified sending address.
Requirements
context.send_email()requirests-sdkv2.8.1 or later, and the email service must be enabled for your environment. If the function is missing, upgrade the SDK. If every call returns a404error, the service is not enabled — contact your customer success manager (CSM).
Before You Begin
To send email from a task script, you need the following:
- A self-service pipeline (SSP) task script. To create one, see Create and Test Scripts.
ts-sdkv2.8.1 or later in your task script's dependencies.- The email service enabled for your organization.
- At least one recipient address.
Send Your First Email
To send an email from a task script, do the following:
- Add
ts-sdkto your task script'spyproject.tomlfile:
[tool.poetry.dependencies]
ts-sdk = "^2.8.1"- Call
context.send_email()from your task script's entry-point function. Thecontextobject is passed in by TDP. You don't import or construct it:
def main(input: dict, context):
context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
)- Publish your task script and run the pipeline. The message arrives from your organization's sending address.
Every parameter is keyword-only. The following call raises a TypeError:
# This does not work. Parameters must be named.
context.send_email(["[email protected]"], "Pipeline run complete")Use It in a python-exec Pipeline
You do not need to build your own task script. The python-exec protocol runs a Python script you paste into the pipeline, and it gives that script the same context object. Call context.send_email() from it directly:
# A python-exec script. `context` and `input` are already defined for you.
result = context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
)
output = {"message_id": result.message_id}Everything on this page applies unchanged: the same parameters, the same limits, the same errors.
Two differences from a task script you build yourself:
- You do not add
ts-sdkto apyproject.tomlfile. The protocol supplies it. - You cannot choose the SDK version.
python-execships whichever version it was last published with, socontext.send_email()is available only oncepython-exechas been republished againstts-sdkv2.8.1 or later. If the function is missing, that is why.
Handle the Result
context.send_email() returns an EmailSendResult object when TDP accepts the message, and raises EmailSendError when it does not. You only need to import EmailSendError, to catch it:
from ts_sdk.task import EmailSendError
def main(input: dict, context):
try:
result = context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
)
except EmailSendError as error:
logger = context.get_logger()
logger.log({"level": "error", "message": f"Email not sent: {error.message}"})
raise
return {"message_id": result.message_id}EmailSendResult has the following attributes.
| Attribute | Type | Description |
|---|---|---|
message_id | string | TDP's identifier for the send. Use it to correlate with audit records. |
ses_message_id | string | The identifier assigned by the underlying email service. |
status | string | Always SENT. |
Parameters
The parameters for context.send_email() appear in the following table. All parameters are keyword-only.
| Parameter | Type | Required | Description |
|---|---|---|---|
to | list of strings | Yes | Primary recipient addresses. |
subject | string | Yes | The subject line, already written out. This function does not fill in templates. Maximum 500 characters. |
body_text | string | See note | The plain text version of the message. |
body_html | string | See note | The HTML version of the message. Mail programs that display HTML show this version instead of body_text. |
cc | list of strings | No | Carbon copy addresses. Everyone on the message can see them. |
bcc | list of strings | No | Blind carbon copy addresses. No other recipient can see them. |
reply_to | string | No | A single address that replies go to. This is not a list. See Set a Reply-To Address. |
attachments | list of EmailAttachment | No | Not yet available. The parameter exists, but sending a non-empty list is rejected. See Attachments. |
idempotency_key | string | No | A value you choose that identifies this message. Sending again with the same key within 24 hours returns the first result instead of sending a second email. See Avoid Sending Twice. |
At least one body is requiredYou must supply
body_text,body_html, or both. A message with neither is rejected.
Limits
TDP enforces the following limits. Exceeding any of them raises EmailSendError and nothing is sent.
| Limit | Value | Error on exceeding |
|---|---|---|
| Recipients | 1–50 across to, cc, and bcc added together, not per field | VALIDATION_FAILED |
| Subject length | 500 characters | VALIDATION_FAILED |
| Message body size | 256 KiB (262,144 bytes) for body_text and body_html added together | MESSAGE_TOO_LARGE |
| Send rate | 100 emails per organization per hour | RATE_LIMITED |
A message addressed to 30 people in to and 25 in cc totals 55 recipients and is rejected, even though neither field exceeds 50 on its own.
Who the Email Comes From
You cannot choose the sending address. TDP resolves it from your organization, and every message from your task scripts is sent from the same verified address. It takes the form noreply@<tenant>.notifications.<your-tdp-domain>, where <tenant> identifies your organization. Instance-level messages use automated in place of the tenant. To see the exact address for your organization, check any message the platform has already sent.
There is no from parameter. If you pass one, TDP ignores it.
Replies to the sending address are not deliveredNobody monitors the sending address, and replies to it bounce. If recipients need to reply, set
reply_toto an address that is monitored.
Set a Reply-To Address
reply_to takes a single address, not a list. When a recipient replies, their mail program addresses the response to that address instead of the sending address:
context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Reply to this message to reach the lab operations team.",
reply_to="[email protected]",
)reply_to cannot be a TetraScience support address. support@, help@, dip@ and [email protected] are rejected with VALIDATION_FAILED, so that pipeline replies do not land in a support queue.
Send to Multiple Recipients
Use to for the people the message is for, cc for people who need a copy, and bcc for recipients that no one else should see:
context.send_email(
to=["[email protected]"],
cc=["[email protected]"],
bcc=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
)In this example, the scientist and the lab manager can each see that the other received the message. Neither can see that [email protected] did.
BCC and repliesA recipient who chooses reply all reaches the
toandccaddresses, but not thebccaddresses. Usebccfor recipients who only need a copy, not for anyone who needs to take part in the conversation.
Send an HTML Message
Supply both body formats when you send HTML. Mail programs that display HTML use body_html, and the rest fall back to body_text:
context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully. Run ID: 1234.",
body_html="<p>Your pipeline finished successfully.</p><p>Run ID: <b>1234</b></p>",
)Both bodies count toward the same 256 KiB limit.
Attachments
Attachments are not yet available. The attachments parameter exists on context.send_email(), but sending a non-empty list is rejected:
EmailSendError: attachments are not yet supported; resubmit without an attachments field.
That error is a 422 with code VALIDATION_FAILED, and no email is sent.
To share a file today, write it to the data lake and put a link to it in the message body.
When attachments are enabled, they will reference data lake files by ID rather than carrying file contents, using an EmailAttachment with a file_id and a filename. Raw bytes will never be accepted.
Avoid Sending Twice
A task script can run more than once for the same input, for example when a step is retried. idempotency_key stops that turning into duplicate email.
Choose a value that identifies the message rather than the attempt, so a retry produces the same key:
context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
idempotency_key=f"run-complete-{context.workflow_id}",
)The first call sends the email. Any later call with the same key, from the same organization, within 24 hours returns the first call's EmailSendResult without sending again. The returned message_id and ses_message_id are the original ones.
A repeat does not count against your rate limitBecause nothing is sent, a repeated key does not consume any of your hourly allowance.
Different keys are different messages. Omitting the key entirely means every call sends.
Errors
EmailSendError has the following attributes.
| Attribute | Type | Description |
|---|---|---|
status_code | integer | The HTTP status TDP returned. |
message | string | A description of what went wrong. |
code | string | A fixed identifier for the failure. None for errors that have no identifier assigned. |
retryable | boolean | True only when retrying the same call can succeed. |
retry_after | integer | The number of seconds to wait before retrying. Set only on a 429. A 503 is retryable but carries no wait, so back off on your own schedule. |
The following table lists every error you can receive and what to do about it.
status_code | code | What happened | What to do |
|---|---|---|---|
| 401 | None | The task's credentials were missing or invalid. | Do not retry. Report the failure. This indicates a platform problem, not a problem with your call. |
| 403 | EMAIL_NOT_ENABLED | Your organization is not set up to send email. | Do not retry. Contact your CSM to have sending enabled for your organization. |
| 404 | None | The email service is not enabled for this environment. | Do not retry. Contact your CSM to have the service enabled. |
| 413 | MESSAGE_TOO_LARGE | The message bodies exceed 256 KiB in total. | Shorten the message, or write the content to the data lake and link to it. |
| 422 | VALIDATION_FAILED | The request broke a rule: too many recipients, no body, an invalid address, an over-long subject, a line break in the subject, a reply_to pointing at a TetraScience support address, or a non-empty attachments list. | Read message to see which rule. Correct the call. Retrying without changing it fails the same way. |
| 429 | RATE_LIMITED | Your organization reached its hourly send limit. | Wait the number of seconds in retry_after, then retry. The count resets on the hour, so retry_after is never more than an hour. |
| 500 | SERVICE_MISCONFIGURED | TDP itself is misconfigured, so the send could not be attempted. | Do not retry. Report the failure. Nothing you change in the call will help. |
| 502 | SES_REJECTED | The email service refused the message, for example an unverified recipient in a test environment. | Do not retry unchanged. Read message, and report it if the cause is not something you control. |
| 502 | SENDER_ROLE_UNAVAILABLE | TDP could not obtain permission to send on your organization's behalf. | Do not retry. Report the failure. |
| 502 | IDENTITY_LOOKUP_FAILED | TDP could not work out which address to send from. | Do not retry. Report the failure. |
| 503 | SES_THROTTLED | The email service is throttling sends across the platform. | retryable is True. Back off and retry. |
| 503 | IDENTITY_LOOKUP_FAILED | The service that knows your sending address is temporarily unreachable. | retryable is True. Retry after a short delay. |
Branch onretryable, not oncode
IDENTITY_LOOKUP_FAILEDappears twice, as a502that will not succeed on retry and as a503that will. Useretryableto decide whether to try again, andcodeonly to explain what happened.
Three errors are worth retrying. A 429 succeeds once the limit resets, so wait retry_after seconds and send again. A 503 is transient, so back off and retry. Everything else repeats until you change the call or someone changes the platform:
from ts_sdk.task import EmailSendError
try:
context.send_email(
to=["[email protected]"],
subject="Pipeline run complete",
body_text="Your pipeline finished successfully.",
)
except EmailSendError as error:
if error.retryable:
# Transient. Safe to try again.
...
raise
A message may have been sentA
502or503raised after the email service was contacted may still have delivered the message. Send with anidempotency_keyso that a retry returns the first result instead of delivering a second copy.
Errors That Do Not Reach the Email Service
TDP checks your call, your organization's settings and its own configuration before it contacts the email service. When you receive a 401, 403, 404, 413, 422, 429 or 500 error, no message was sent to anyone, including recipients whose addresses were valid.
IDENTITY_LOOKUP_FAILED and SENDER_ROLE_UNAVAILABLE also stop before the email service is contacted, even though they are reported as 502 or 503.
Only SES_REJECTED and SES_THROTTLED mean the email service was reached.
Limitations
- Task scripts are the only artifacts that can send email. Data apps cannot.
- You cannot choose the sending address.
- Attachments are not yet available. Sending a non-empty
attachmentslist is rejected. - Your organization must be set up for sending. Without that, every send returns
403withEMAIL_NOT_ENABLED.
Related Documentation
Updated 1 day ago

