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() requires ts-sdk v2.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 a 404 error, 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-sdk v2.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:

  1. Add ts-sdk to your task script's pyproject.toml file:
[tool.poetry.dependencies]
ts-sdk = "^2.8.1"
  1. Call context.send_email() from your task script's entry-point function. The context object 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.",
    )
  1. 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-sdk to a pyproject.toml file. The protocol supplies it.
  • You cannot choose the SDK version. python-exec ships whichever version it was last published with, so context.send_email() is available only once python-exec has been republished against ts-sdk v2.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.

AttributeTypeDescription
message_idstringTDP's identifier for the send. Use it to correlate with audit records.
ses_message_idstringThe identifier assigned by the underlying email service.
statusstringAlways SENT.

Parameters

The parameters for context.send_email() appear in the following table. All parameters are keyword-only.

ParameterTypeRequiredDescription
tolist of stringsYesPrimary recipient addresses.
subjectstringYesThe subject line, already written out. This function does not fill in templates. Maximum 500 characters.
body_textstringSee noteThe plain text version of the message.
body_htmlstringSee noteThe HTML version of the message. Mail programs that display HTML show this version instead of body_text.
cclist of stringsNoCarbon copy addresses. Everyone on the message can see them.
bcclist of stringsNoBlind carbon copy addresses. No other recipient can see them.
reply_tostringNoA single address that replies go to. This is not a list. See Set a Reply-To Address.
attachmentslist of EmailAttachmentNoNot yet available. The parameter exists, but sending a non-empty list is rejected. See Attachments.
idempotency_keystringNoA 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 required

You 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.

LimitValueError on exceeding
Recipients1–50 across to, cc, and bcc added together, not per fieldVALIDATION_FAILED
Subject length500 charactersVALIDATION_FAILED
Message body size256 KiB (262,144 bytes) for body_text and body_html added togetherMESSAGE_TOO_LARGE
Send rate100 emails per organization per hourRATE_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 delivered

Nobody monitors the sending address, and replies to it bounce. If recipients need to reply, set reply_to to 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 replies

A recipient who chooses reply all reaches the to and cc addresses, but not the bcc addresses. Use bcc for 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 limit

Because 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.

AttributeTypeDescription
status_codeintegerThe HTTP status TDP returned.
messagestringA description of what went wrong.
codestringA fixed identifier for the failure. None for errors that have no identifier assigned.
retryablebooleanTrue only when retrying the same call can succeed.
retry_afterintegerThe 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_codecodeWhat happenedWhat to do
401NoneThe task's credentials were missing or invalid.Do not retry. Report the failure. This indicates a platform problem, not a problem with your call.
403EMAIL_NOT_ENABLEDYour organization is not set up to send email.Do not retry. Contact your CSM to have sending enabled for your organization.
404NoneThe email service is not enabled for this environment.Do not retry. Contact your CSM to have the service enabled.
413MESSAGE_TOO_LARGEThe message bodies exceed 256 KiB in total.Shorten the message, or write the content to the data lake and link to it.
422VALIDATION_FAILEDThe 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.
429RATE_LIMITEDYour 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.
500SERVICE_MISCONFIGUREDTDP itself is misconfigured, so the send could not be attempted.Do not retry. Report the failure. Nothing you change in the call will help.
502SES_REJECTEDThe 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.
502SENDER_ROLE_UNAVAILABLETDP could not obtain permission to send on your organization's behalf.Do not retry. Report the failure.
502IDENTITY_LOOKUP_FAILEDTDP could not work out which address to send from.Do not retry. Report the failure.
503SES_THROTTLEDThe email service is throttling sends across the platform.retryable is True. Back off and retry.
503IDENTITY_LOOKUP_FAILEDThe service that knows your sending address is temporarily unreachable.retryable is True. Retry after a short delay.
📘

Branch on retryable, not on code

IDENTITY_LOOKUP_FAILED appears twice, as a 502 that will not succeed on retry and as a 503 that will. Use retryable to decide whether to try again, and code only 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 sent

A 502 or 503 raised after the email service was contacted may still have delivered the message. Send with an idempotency_key so 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 attachments list is rejected.
  • Your organization must be set up for sending. Without that, every send returns 403 with EMAIL_NOT_ENABLED.

Related Documentation


Did this page help you?